diff --git a/CHANGE-LOG.md b/CHANGE-LOG.md index cb8d4da2..64cdb7f0 100644 --- a/CHANGE-LOG.md +++ b/CHANGE-LOG.md @@ -1,5 +1,97 @@ # OpenStudyBuilder (OSB) Commits changelog +## V 2.5 + +New Features and Enhancements +============ + +### Fixes and Enhancements + +- The menu Library > Concepts > Activities > Activity Instances, now support editing of core attributes and associated activity items. +- The menu Library > Concepts > Activities > Activity Instances, now support defining the Activity Instance Class Categoric Finding including relationship to Categoric Response terms. +- Tables action buttons now expand on hover. +- Online help pop-ups are now movable by drag and drop (like dialog windows). + +### New Feature + +- The new "CRF Library Versions" report in NeoDash compares two versions of CRF forms in the CRF library. +- The system now supports the ability to develop UI and API extensions that can be deployed as an integrated part of the OSB system. See more details in the following readme files within the GitHub repository: + https://github.com/NovoNordisk-OpenSource/openstudybuilder-solution/tree/main/clinical-mdr-api/exte… + https://github.com/NovoNordisk-OpenSource/openstudybuilder-solution/tree/main/studybuilder/src/exte… + +### Performance Improvements +- Faster UI performance of Detailed SoA page. +- Faster saving of audit trail information when creating/editing/deleting study selections, e.g. Study Activities, Visits, Epochs, Arms etc. +- Faster page load when opening detail page of activity instance classes. +- Faster loading and filtering of Library tables: Codelists, Activities. + +### End-to-End Automated test enhancements +- Various code improvements to ensure easier maintenance and overall tests stability. +- Library > Concepts > Activities > Activities: Defined and implemented tests for multiple instance allowed checkbox. +- Studies > Define Study > Study Structure > Study Arms: Defined and implemented tests for study arm label. +- Studies > Define Study > Study Activities > Study Activities: Updated tests to reflect change to placeholder handling actions. +- Studies > Define Study > Study Activities > SoA > Detailed view: Defined and implemented tests for data exports. +- Studies > Define Study > Study Activities > SoA > Protocol view: Defined and implemented tests for data exports. +- Studies > Define Study > Study Interventions > Study Compounds: Defined and implemented tests for Study Compunds. +- Studies > Define Study > Study Interventions > Study Compound Dosings: Defined and implemented tests for Study Compunds Dosings. +- Studies > Study List > Copy Study: Updated tests to use custom test study with fully defined structured created via API. +- Reports > Neodash: Defined and implemened tests for CRFs versioning. + +Solved Bugs +============ + +### API + +- Fix ODM Item to Term relationship + +### About Page + +- UI fetches cached (i.e. old) SBOMs by default + +### Library + + **API** + +- CT Terms order not retained +- Fix Odm Vendor Extension versioning + + **Concepts > Activity Instances > Overview Page** + +- Broken layout when topic code is very long + + **Data Collection Standards -> CRF Builder** + +- When you provide a wrong value for an attribute of a vendor extension, then we you save the item is created but the error is displayed, creating duplicate + + **Data Collection Standards > CRF Builder > CRF Items** + +- Unable to assign term in Step 3 + + **Data Collection Standards > CRF Builder > Items** + +- Broken pagination for Units table +- CRF Item History page is not displaying the Number of Row + +### Studies + + **Define Study -> Study Structure -> Disease Milestones** + +- Error thrown when performing search + + **Define Study > Study Activities** + +- Cannot edit subgroup of activity placeholder +- Issue with searching activity by name + + **Define Study > Study Activities > Detailed SoA** + +- Unable to reorder activities in 'patient reported outcome' sub-group + + **Define Study > Study Structure > Study Visits** + +- Unique visit number is not always unique + + ## V 2.4 New Features and Enhancements diff --git a/clinical-mdr-api/.coveragerc b/clinical-mdr-api/.coveragerc index c97d3416..90fe25a2 100644 --- a/clinical-mdr-api/.coveragerc +++ b/clinical-mdr-api/.coveragerc @@ -6,3 +6,4 @@ include = clinical_mdr_api/* consumer_api/* common/* + extensions/* 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 index f3a6fdde..a04041ed 100644 --- a/clinical-mdr-api/.github/agents/test-specialist-integration-api.agent.md +++ b/clinical-mdr-api/.github/agents/test-specialist-integration-api.agent.md @@ -1,5 +1,5 @@ --- -name: Intergration API Test Specialist +name: Integration 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. --- diff --git a/clinical-mdr-api/.gitignore b/clinical-mdr-api/.gitignore index 40b677ba..9c2b26db 100644 --- a/clinical-mdr-api/.gitignore +++ b/clinical-mdr-api/.gitignore @@ -28,5 +28,6 @@ linting_report.txt .coverage* /reports /consumer_api/reports +/extensions/reports traceability.html Consumer_API_Traceability.html \ No newline at end of file diff --git a/clinical-mdr-api/Pipfile b/clinical-mdr-api/Pipfile index 463465c0..ac6c59fe 100644 --- a/clinical-mdr-api/Pipfile +++ b/clinical-mdr-api/Pipfile @@ -73,18 +73,18 @@ dist = "python setup.py bdist_wheel" package = "pip wheel -r requirements.txt -w dist" testunit = "pytest -s --cov-report html:reports/coverage-unit-html --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/unit_report.xml clinical_mdr_api/tests/unit/ common/tests/unit" testint = "pytest -s -n 4 --dist loadfile --cov-report html:reports/coverage-int-html --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/int_report.xml clinical_mdr_api/tests/integration/" -testauth = "pytest -s --cov-report html:reports/coverage-auth-html --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/auth_report.xml clinical_mdr_api/tests/auth/ common/tests/auth" +testauth = "pytest -s --cov-report html:reports/coverage-auth-html --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/auth_report.xml clinical_mdr_api/tests/auth/ common/tests/auth extensions/tests/auth/" test-telemetry = "pytest -s --cov-report html:reports/coverage-telemetry-html --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/telemetry_report.xml clinical_mdr_api/tests/telemetry/" testunitallure = "pytest -s --cov-report html:reports/coverage-unit --cov-report xml:reports/coverage.xml --cov-append --cov=clinical_mdr_api --junitxml=reports/unit_report.xml --alluredir reports/allure-results clinical_mdr_api/tests/unit/" testintallure = "pytest -s -n 4 --dist loadfile --cov-report html:reports/coverage-int --cov-report xml:reports/coverage.xml --cov-append --cov=clinical_mdr_api --junitxml=reports/int_report.xml --alluredir reports/allure-results clinical_mdr_api/tests/integration/" -lint = "pylint -j 0 clinical_mdr_api consumer_api common" -mypy = "mypy clinical_mdr_api consumer_api common" -sblint = "python -m sblint.main clinical_mdr_api consumer_api common" -black = "python -m black clinical_mdr_api consumer_api common" -isort = "python -m isort clinical_mdr_api consumer_api common" +lint = "pylint -j 0 clinical_mdr_api consumer_api common extensions" +mypy = "mypy clinical_mdr_api consumer_api common extensions" +sblint = "python -m sblint.main clinical_mdr_api consumer_api common extensions" +black = "python -m black clinical_mdr_api consumer_api common extensions" +isort = "python -m isort clinical_mdr_api consumer_api common extensions" format = """sh -c " - python -m isort clinical_mdr_api consumer_api common \ - && python -m black clinical_mdr_api consumer_api common \ + python -m isort clinical_mdr_api consumer_api common extensions \ + && python -m black clinical_mdr_api consumer_api common extensions \ " """ openapi = "python generate_openapi_json.py" @@ -128,3 +128,34 @@ consumer-api-schemathesis = """ consumer_api/openapi.json """ consumer-api-traceability = "python consumer_api/requirements/generate_traceability.py" + +extensions-api-dev = "uvicorn --host=0.0.0.0 --port=8009 extensions.extensions_api:app --reload" +extensions-openapi = "python generate_openapi.py extensions.extensions_api:app extensions/openapi.json extensions/apiVersion" +extensions-lint = "pylint extensions" +extensions-test = """sh -c " + PRODEX_API_MOCK=True pytest -s -n 1 --dist loadfile \ + --cov-report html:extensions/reports/coverage-extensions-html \ + --cov-report xml:extensions/reports/coverage.xml \ + --cov-append --cov \ + --junitxml=extensions/reports/test_report.xml \ + --ignore=extensions/tests/auth \ + extensions/tests extensions/*/tests +" +""" +extensions-testauth = "pytest -s -n 1 --dist loadfile --cov-report html:extensions/reports/coverage-auth-html --cov-report xml:extensions/reports/coverage.xml --cov-append --cov --junitxml=extensions/reports/auth_report.xml extensions/tests/auth" +extensions-schemathesis = """ + schemathesis + run + --experimental=openapi-3.1 + --checks=all + --base-url=http://localhost:8009 + --request-timeout=30000 + --hypothesis-max-examples=100 + --hypothesis-verbosity=normal + --hypothesis-deadline=None + --hypothesis-suppress-health-check=too_slow,filter_too_much,data_too_large + --junit-xml=extensions/reports/schemathesis_report.xml + --report=extensions/reports/schemathesis_report.tgz + --show-trace + extensions/openapi.json +""" diff --git a/clinical-mdr-api/apiVersion b/clinical-mdr-api/apiVersion index 49ee0744..e581be7f 100644 --- a/clinical-mdr-api/apiVersion +++ b/clinical-mdr-api/apiVersion @@ -1 +1 @@ -3.0.569 +3.0.595 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 654528b6..ceb79d18 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 @@ -590,3 +590,13 @@ def get_activity_item_classes( results, meta = db.cypher_query(final_query, params=params) return [dict(zip(meta, row)) for row in results], total + + def get_all_version_numbers(self, uid: str) -> list[str]: + """Get all version numbers of a given activity instance class""" + + rs, _ = db.cypher_query( + "MATCH (:ActivityInstanceClassRoot {uid: $uid})-[hv:HAS_VERSION]-(:ActivityInstanceClassValue) RETURN DISTINCT hv.version", + params={"uid": uid}, + ) + + return [row[0] for row in rs] 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 ba1b0028..c3e80967 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 @@ -16,6 +16,7 @@ ActivityItemClassValue, ) from clinical_mdr_api.domain_repositories.models.controlled_terminology import ( + CTCodelistRoot, CTTermRoot, ) from clinical_mdr_api.domain_repositories.models.generic import ( @@ -482,6 +483,16 @@ def patch_mappings(self, uid: str, variable_class_uids: list[str]) -> None: variable_class = VariableClass.nodes.get(uid=variable_class) root.maps_variable_class.connect(variable_class) + @sb_clear_cache(caches=["cache_store_item_by_uid"]) + def patch_valid_codelist_mappings( + self, uid: str, valid_codelist_uids: list[str] + ) -> None: + root = ActivityItemClassRoot.nodes.get(uid=uid) + root.has_valid_codelist_for_items.disconnect_all() + for codelist_uid in valid_codelist_uids: + codelist = CTCodelistRoot.nodes.get(uid=codelist_uid) + root.has_valid_codelist_for_items.connect(codelist) + def _maintain_parameters( self, versioned_object: ActivityItemClassAR, @@ -620,6 +631,45 @@ def get_referenced_codelist_and_term_uids( codelists_and_terms[cl_uid] = term_uids return codelists_and_terms + def get_valid_codelists_and_terms( + self, activity_item_class_uid: str, ct_catalogue_name: str | None + ) -> dict[str, list[str] | None]: + if ct_catalogue_name: + extra_filter_kwargs = { + "has_valid_codelist_for_items__has_codelist__name": ct_catalogue_name, + } + else: + extra_filter_kwargs = {} + codelist_uids = ( + ActivityItemClassRoot() + .nodes.filter( + uid=activity_item_class_uid, + **extra_filter_kwargs, + ) + .traverse( + Path( + value="has_valid_codelist_for_items", + include_rels_in_return=False, + include_nodes_in_return=False, + ), + ) + .unique_variables("has_valid_codelist_for_items") + .intermediate_transform( + { + "codelist_uid": { + "source": NodeNameResolver("has_valid_codelist_for_items"), + "source_prop": "uid", + "include_in_return": True, + }, + }, + distinct=True, + ) + .all() + ) + # Return a dictionary of codelist uids and filtered terms + # Note that for now, this model does not cater for filtering down to a subset of terms + return {uid: None for uid in codelist_uids} + def get_activity_instance_classes_using_item( self, activity_item_class_uid: str, 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 ee55ff11..b352e342 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 @@ -131,7 +131,8 @@ def _create_new_value_node(self, ar: ActivityInstanceAR) -> ActivityInstanceValu else False ) activity_item_node = ActivityItem( - is_adam_param_specific=is_adam_param_specific + is_adam_param_specific=is_adam_param_specific, + text_value=item.text_value, ) activity_item_node.save() activity_item_node.has_activity_item_class.connect(activity_item_class) @@ -162,6 +163,7 @@ 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}, + "text_value": item.text_value, } ) @@ -186,6 +188,7 @@ def _has_item_data_changed(self, ar_items, value_item_nodes): (ct_term["uid"], ct_term["codelist_uid"]) for ct_term in ct_terms }, + "text_value": activity_item_node.text_value, } ) for item in ar_activity_items: @@ -424,6 +427,7 @@ def _create_aggregate_root_instance_from_cypher_result( ) for unit in activity_item.get("unit_definitions") ], + text_value=activity_item.get("text_value"), ) for activity_item in input_dict.get("activity_items", []) ], @@ -502,6 +506,7 @@ 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, + text_value=activity_item.text_value, ) ) activity_groupings_nodes = value.has_activity.all() @@ -632,6 +637,7 @@ def _create_ar( activity_item_class_name=activity_item["activity_item_class_name"], ct_terms=ct_terms, unit_definitions=unit_definitions, + text_value=activity_item.get("text_value"), ) ) activity_groupings = [] @@ -738,7 +744,8 @@ 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 + is_adam_param_specific: activity_item.is_adam_param_specific, + text_value: activity_item.text_value }] 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) @@ -978,7 +985,8 @@ 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 + is_adam_param_specific: activity_item.is_adam_param_specific, + text_value: activity_item.text_value } ]) AS activity_items CALL { 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 9b14b12c..27c0a650 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 @@ -31,7 +31,9 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, sb_clear_cache, + validate_filter_by_dict, validate_filters_and_add_search_string, ) @@ -121,6 +123,42 @@ def generic_match_clause(self, **kwargs): return f"""CYPHER runtime=slotted MATCH (concept_root:{concept_label})-[{rel}]->(concept_value:{concept_value_label})""" + def minimal_count_query( + self, + filter_by: dict[str, dict[str, Any]] | None, + return_all_versions: bool, + **kwargs, + ) -> tuple[str | None, dict[str, Any]]: + concept_label = self.root_class.__label__ + concept_value_label = self.value_class.__label__ + + # we do not support minimal count queries when a specific version is requested + if kwargs.get("version", None) is not None: + return None, {} + + filter_by = validate_filter_by_dict(filter_by) + # no filters, we can return a minimal count query + if not filter_by or len(filter_by) == 0: + if return_all_versions: + # when all versions are requested, we need to count the Value nodes + query = f"""MATCH (concept_root:{concept_label})-[hv:HAS_VERSION]->(concept_value:{concept_value_label}) RETURN count(DISTINCT hv) AS count""" + return query, {} + # when only latest versions are requested, we can count the root nodes directly + query = f"""MATCH (concept_root:{concept_label}) RETURN count(DISTINCT concept_root) AS count""" + return query, {} + + # if the only filter is for status, we return a specific count query + if len(filter_by) == 1 and "status" in filter_by and not return_all_versions: + query = f""" + MATCH (concept_root:{concept_label})-[hv:HAS_VERSION]->(concept_value:{concept_value_label}) + WHERE hv.end_date IS NULL AND hv.status IN $status + RETURN count(DISTINCT concept_root) AS count + """ + params = {"status": filter_by["status"]["v"]} + return query, params + # else, a generic count query needs to be used + return None, {} + def generic_alias_clause(self, **kwargs): version = kwargs.get("version", None) where_version = ( @@ -325,6 +363,12 @@ def find_all( if not return_all_versions else self.generic_alias_clause_all_versions() ) + self.specific_alias_clause(**kwargs) + + minimal_count_query, minimal_count_params = self.minimal_count_query( + filter_by=filter_by, return_all_versions=return_all_versions, **kwargs + ) + filtering_active = minimal_count_query is None + query = CypherQueryBuilder( match_clause=match_clause, alias_clause=alias_clause, @@ -336,6 +380,7 @@ def find_all( total_count=total_count, return_model=self.return_model, format_filter_sort_keys=self.format_filter_sort_keys, + one_element_extra=filtering_active, ) if kwargs.get("version", None) is not None: @@ -347,14 +392,23 @@ def find_all( extracted_items = self._retrieve_concepts_from_cypher_res( result_array, attributes_names ) - - 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 + total_amount = calculate_total_count_from_query_result( + len(extracted_items), + page_number, + page_size, + total_count, + extra_requested=filtering_active, ) - + if 0 < page_size < len(extracted_items): + extracted_items = extracted_items[:page_size] + if total_amount is None: + if minimal_count_query is not None: + count_result, _ = db.cypher_query( + query=minimal_count_query, params=minimal_count_params + ) + total_amount = count_result[0][0] if len(count_result) > 0 else 0 + else: + total_amount = -1 return extracted_items, total_amount def _retrieve_concepts_from_cypher_res( diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/condition_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/condition_repository.py index a6168511..25798bc8 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/condition_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/condition_repository.py @@ -26,8 +26,8 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmFormalExpressionModel, + OdmTranslatedTextModel, ) from clinical_mdr_api.models.concepts.odms.odm_condition import OdmCondition from common.utils import convert_to_datetime @@ -58,15 +58,13 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( ) for formal_expression_value in value.has_formal_expression.all() ], - descriptions=[ - OdmDescriptionModel( - name=description_value.name, - language=description_value.language, - description=description_value.description, - instruction=description_value.instruction, - sponsor_instruction=description_value.sponsor_instruction, + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text_value.text_type, + language=translated_text_value.language, + text=translated_text_value.text, ) - for description_value in value.has_description.all() + for translated_text_value in value.has_translated_text.all() ], aliases=[ OdmAliasModel(name=alias_value.name, context=alias_value.context) @@ -96,17 +94,13 @@ def _create_aggregate_root_instance_from_cypher_result( ) for formal_expression in input_dict["formal_expressions"] ], - descriptions=[ - OdmDescriptionModel( - name=description["name"], - language=description.get("language", None), - description=description.get("description", None), - instruction=description.get("instruction", None), - sponsor_instruction=description.get( - "sponsor_instruction", None - ), + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text["text_type"], + language=translated_text["language"], + text=translated_text["text"], ) - for description in input_dict["descriptions"] + for translated_text in input_dict["translated_texts"] ], aliases=[ OdmAliasModel(name=alias["name"], context=alias["context"]) @@ -140,8 +134,7 @@ def specific_alias_clause(self, **kwargs) -> str: [(concept_value)-[:HAS_FORMAL_EXPRESSION]->(fev:OdmFormalExpression) | {context: fev.context, expression: fev.expression}] AS formal_expressions, -[(concept_value)-[:HAS_DESCRIPTION]->(dv:OdmDescription) | -{name: dv.name, language: dv.language, description: dv.description, instruction: dv.instruction, sponsor_instruction: dv.sponsor_instruction}] AS descriptions, +[(concept_value)-[:HAS_TRANSLATED_TEXT]->(dv:OdmTranslatedText) | {text_type: dv.text_type, language: dv.language, text: dv.text}] AS translated_texts, [(concept_value)-[:HAS_ALIAS]->(av:OdmAlias) | {name: av.name, context: av.context}] AS aliases """ @@ -152,7 +145,7 @@ def _get_or_create_value( new_value = super()._get_or_create_value(root, ar, force_new_value_node) self.connect_aliases(ar.concept_vo.aliases, new_value) - self.connect_descriptions(ar.concept_vo.descriptions, new_value) + self.connect_translated_texts(ar.concept_vo.translated_texts, new_value) self.connect_formal_expressions(ar.concept_vo.formal_expressions, new_value) return new_value @@ -176,15 +169,13 @@ def _has_data_changed(self, ar: OdmConditionAR, value: OdmConditionValue) -> boo ) for formal_expression_node in value.has_formal_expression.all() } - description_nodes = { - OdmDescriptionModel( - name=description_node.name, - language=description_node.language, - description=description_node.description, - instruction=description_node.instruction, - sponsor_instruction=description_node.sponsor_instruction, + translated_text_nodes = { + OdmTranslatedTextModel( + text_type=translated_text_node.text_type, + language=translated_text_node.language, + text=translated_text_node.text, ) - for description_node in value.has_description.all() + for translated_text_node in value.has_translated_text.all() } alias_nodes = { OdmAliasModel(name=alias_node.name, context=alias_node.context) @@ -193,7 +184,7 @@ def _has_data_changed(self, ar: OdmConditionAR, value: OdmConditionValue) -> boo are_rels_changed = ( set(ar.concept_vo.formal_expressions) != formal_expression_nodes - or set(ar.concept_vo.descriptions) != description_nodes + or set(ar.concept_vo.translated_texts) != translated_text_nodes or set(ar.concept_vo.aliases) != alias_nodes ) 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 b400e276..e4e11970 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 @@ -24,7 +24,7 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmTranslatedTextModel, ) from clinical_mdr_api.models.concepts.odms.odm_form import OdmForm from clinical_mdr_api.services._utils import ensure_transaction @@ -51,15 +51,13 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( name=value.name, sdtm_version=value.sdtm_version, repeating=value.repeating, - descriptions=[ - OdmDescriptionModel( - name=description_value.name, - language=description_value.language, - description=description_value.description, - instruction=description_value.instruction, - sponsor_instruction=description_value.sponsor_instruction, + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text_value.text_type, + language=translated_text_value.language, + text=translated_text_value.text, ) - for description_value in value.has_description.all() + for translated_text_value in value.has_translated_text.all() ], aliases=[ OdmAliasModel(name=alias_value.name, context=alias_value.context) @@ -108,17 +106,13 @@ def _create_aggregate_root_instance_from_cypher_result( name=input_dict["name"], sdtm_version=input_dict.get("sdtm_version"), repeating=input_dict.get("repeating"), - descriptions=[ - OdmDescriptionModel( - name=description["name"], - language=description.get("language", None), - description=description.get("description", None), - instruction=description.get("instruction", None), - sponsor_instruction=description.get( - "sponsor_instruction", None - ), + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text["text_type"], + language=translated_text["language"], + text=translated_text["text"], ) - for description in input_dict["descriptions"] + for translated_text in input_dict["translated_texts"] ], aliases=[ OdmAliasModel(name=alias["name"], context=alias["context"]) @@ -158,8 +152,7 @@ def specific_alias_clause(self, **kwargs) -> str: toString(concept_value.repeating) AS repeating, concept_value.sdtm_version AS sdtm_version, -[(concept_value)-[:HAS_DESCRIPTION]->(dv:OdmDescription) | -{name: dv.name, language: dv.language, description: dv.description, instruction: dv.instruction, sponsor_instruction: dv.sponsor_instruction}] AS descriptions, +[(concept_value)-[:HAS_TRANSLATED_TEXT]->(dv:OdmTranslatedText) | {text_type: dv.text_type, language: dv.language, text: dv.text}] AS translated_texts, [(concept_value)-[:HAS_ALIAS]->(av:OdmAlias) | {name: av.name, context: av.context}] AS aliases, @@ -221,7 +214,7 @@ def _get_or_create_value( self.manage_vendor_relationships( current_latest, new_value, ar.should_disconnect_relationships ) - self.connect_descriptions(ar.concept_vo.descriptions, new_value) + self.connect_translated_texts(ar.concept_vo.translated_texts, new_value) self.connect_aliases(ar.concept_vo.aliases, new_value) return new_value @@ -240,15 +233,13 @@ def _create_new_value_node(self, ar: OdmFormAR) -> OdmFormValue: def _has_data_changed(self, ar: OdmFormAR, value: OdmFormValue) -> bool: are_concept_properties_changed = super()._has_data_changed(ar=ar, value=value) - description_nodes = { - OdmDescriptionModel( - name=description_node.name, - language=description_node.language, - description=description_node.description, - instruction=description_node.instruction, - sponsor_instruction=description_node.sponsor_instruction, + translated_text_nodes = { + OdmTranslatedTextModel( + text_type=translated_text_node.text_type, + language=translated_text_node.language, + text=translated_text_node.text, ) - for description_node in value.has_description.all() + for translated_text_node in value.has_translated_text.all() } alias_nodes = { OdmAliasModel(name=alias_node.name, context=alias_node.context) @@ -256,7 +247,7 @@ def _has_data_changed(self, ar: OdmFormAR, value: OdmFormValue) -> bool: } are_rels_changed = ( - set(ar.concept_vo.descriptions) != description_nodes + set(ar.concept_vo.translated_texts) != translated_text_nodes or set(ar.concept_vo.aliases) != alias_nodes ) @@ -282,6 +273,7 @@ def find_by_uid_with_study_event_relation( WITH value, ref, collect(hv_rel) AS hv_rels RETURN + value.oid AS oid, value.name AS name, hv_rels[0].version AS version, ref.order_number AS order_number, @@ -298,13 +290,14 @@ def find_by_uid_with_study_event_relation( return OdmFormRefVO.from_repository_values( uid=uid, - name=rs[0][0], - version=rs[0][1], + oid=rs[0][0], + name=rs[0][1], study_event_uid=study_event_uid, - order_number=rs[0][2], - mandatory=rs[0][3], - locked=rs[0][4], - collection_exception_condition_oid=rs[0][5], + version=rs[0][2], + order_number=rs[0][3], + mandatory=rs[0][4], + locked=rs[0][5], + collection_exception_condition_oid=rs[0][6], ) @ensure_transaction(db) 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 cbf2507e..7ca856d1 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 @@ -34,7 +34,7 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmTranslatedTextModel, ) from clinical_mdr_api.models.concepts.odms.odm_item_group import OdmItemGroup from clinical_mdr_api.services._utils import ensure_transaction @@ -72,15 +72,13 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( origin=value.origin, purpose=value.purpose, comment=value.comment, - descriptions=[ - OdmDescriptionModel( - name=description_value.name, - language=description_value.language, - description=description_value.description, - instruction=description_value.instruction, - sponsor_instruction=description_value.sponsor_instruction, + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text_value.text_type, + language=translated_text_value.language, + text=translated_text_value.text, ) - for description_value in value.has_description.all() + for translated_text_value in value.has_translated_text.all() ], aliases=[ OdmAliasModel(name=alias_value.name, context=alias_value.context) @@ -134,17 +132,13 @@ def _create_aggregate_root_instance_from_cypher_result( origin=input_dict.get("origin"), purpose=input_dict.get("purpose"), comment=input_dict.get("comment"), - descriptions=[ - OdmDescriptionModel( - name=description["name"], - language=description.get("language", None), - description=description.get("description", None), - instruction=description.get("instruction", None), - sponsor_instruction=description.get( - "sponsor_instruction", None - ), + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text["text_type"], + language=translated_text["language"], + text=translated_text["text"], ) - for description in input_dict["descriptions"] + for translated_text in input_dict["translated_texts"] ], aliases=[ OdmAliasModel(name=alias["name"], context=alias["context"]) @@ -189,8 +183,7 @@ def specific_alias_clause(self, **kwargs) -> str: concept_value.purpose AS purpose, concept_value.comment AS comment, -[(concept_value)-[:HAS_DESCRIPTION]->(dv:OdmDescription) | -{name: dv.name, language: dv.language, description: dv.description, instruction: dv.instruction, sponsor_instruction: dv.sponsor_instruction}] AS descriptions, +[(concept_value)-[:HAS_TRANSLATED_TEXT]->(dv:OdmTranslatedText) | {text_type: dv.text_type, language: dv.language, text: dv.text}] AS translated_texts, [(concept_value)-[:HAS_ALIAS]->(av:OdmAlias) | {name: av.name, context: av.context}] AS aliases, @@ -259,7 +252,7 @@ def _get_or_create_value( self.manage_vendor_relationships( current_latest, new_value, ar.should_disconnect_relationships ) - self.connect_descriptions(ar.concept_vo.descriptions, new_value) + self.connect_translated_texts(ar.concept_vo.translated_texts, new_value) self.connect_aliases(ar.concept_vo.aliases, new_value) for sdtm_domain_uid in ar.concept_vo.sdtm_domain_uids: @@ -293,15 +286,13 @@ def _create_new_value_node(self, ar: OdmItemGroupAR) -> OdmItemGroupValue: def _has_data_changed(self, ar: OdmItemGroupAR, value: OdmItemGroupValue) -> bool: are_concept_properties_changed = super()._has_data_changed(ar=ar, value=value) - description_nodes = { - OdmDescriptionModel( - name=description_node.name, - language=description_node.language, - description=description_node.description, - instruction=description_node.instruction, - sponsor_instruction=description_node.sponsor_instruction, + translated_text_nodes = { + OdmTranslatedTextModel( + text_type=translated_text_node.text_type, + language=translated_text_node.language, + text=translated_text_node.text, ) - for description_node in value.has_description.all() + for translated_text_node in value.has_translated_text.all() } alias_nodes = { OdmAliasModel(name=alias_node.name, context=alias_node.context) @@ -314,7 +305,7 @@ def _has_data_changed(self, ar: OdmItemGroupAR, value: OdmItemGroupValue) -> boo } are_rels_changed = ( - set(ar.concept_vo.descriptions) != description_nodes + set(ar.concept_vo.translated_texts) != translated_text_nodes or set(ar.concept_vo.aliases) != alias_nodes or set(ar.concept_vo.sdtm_domain_uids) != sdtm_domain_uids ) 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 324c60ec..737aed04 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 @@ -34,9 +34,9 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmTranslatedTextModel, ) -from clinical_mdr_api.models.concepts.odms.odm_item import OdmItem +from clinical_mdr_api.models.concepts.odms.odm_item import OdmItem, OdmItemCodelist from clinical_mdr_api.services._utils import ensure_transaction from clinical_mdr_api.utils import db_result_to_list from common.exceptions import NotFoundException @@ -61,7 +61,7 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( """ 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) + MATCH (ai)<-[:CONTAINS_ACTIVITY_ITEM]-(aiv: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) @@ -70,6 +70,9 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( MATCH (ai)<-[:HAS_ACTIVITY_ITEM]-(aicr:ActivityItemClassRoot) RETURN DISTINCT air.uid AS activity_instance_uid, + aiv.name AS activity_instance_name, + aiv.adam_param_code AS activity_instance_adam_param_code, + aiv.topic_code AS activity_instance_topic_code, aicr.uid AS activity_item_class_uid, ofr.uid AS odm_form_uid, oigr.uid AS odm_item_group_uid, @@ -97,15 +100,13 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( sds_var_name=value.sds_var_name, origin=value.origin, comment=value.comment, - descriptions=[ - OdmDescriptionModel( - name=description_value.name, - language=description_value.language, - description=description_value.description, - instruction=description_value.instruction, - sponsor_instruction=description_value.sponsor_instruction, + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text_value.text_type, + language=translated_text_value.language, + text=translated_text_value.text, ) - for description_value in value.has_description.all() + for translated_text_value in value.has_translated_text.all() ], aliases=[ OdmAliasModel(name=alias_value.name, context=alias_value.context) @@ -115,7 +116,17 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( unit_definition.uid for unit_definition in value.has_unit_definition.all() ], - codelist_uid=codelist.uid if codelist else None, + codelist=( + OdmItemCodelist( + uid=codelist.uid, + allows_multi_choice=value.has_codelist.relationship( + codelist + ).allows_multi_choice + or False, + ) + if codelist + else None + ), term_uids=[ term.uid for term_context in value.has_codelist_term.all() @@ -166,24 +177,30 @@ def _create_aggregate_root_instance_from_cypher_result( sds_var_name=input_dict.get("sds_var_name"), origin=input_dict.get("origin"), comment=input_dict.get("comment"), - descriptions=[ - OdmDescriptionModel( - name=description["name"], - language=description.get("language", None), - description=description.get("description", None), - instruction=description.get("instruction", None), - sponsor_instruction=description.get( - "sponsor_instruction", None - ), + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text["text_type"], + language=translated_text["language"], + text=translated_text["text"], ) - for description in input_dict["descriptions"] + for translated_text in input_dict["translated_texts"] ], aliases=[ OdmAliasModel(name=alias["name"], context=alias["context"]) for alias in input_dict["aliases"] ], unit_definition_uids=input_dict["unit_definition_uids"], - codelist_uid=input_dict["codelist_uid"], + codelist=( + OdmItemCodelist( + uid=input_dict["codelist"]["uid"], + allows_multi_choice=input_dict["codelist"][ + "allows_multi_choice" + ] + or False, + ) + if input_dict.get("codelist", None) + else None + ), term_uids=input_dict["term_uids"], activity_instances=input_dict["activity_instances"], vendor_element_uids=input_dict["vendor_element_uids"], @@ -225,20 +242,19 @@ def specific_alias_clause(self, **kwargs) -> str: concept_value.origin as origin, concept_value.comment as comment, -[(concept_value)-[:HAS_DESCRIPTION]->(dv:OdmDescription) | -{name: dv.name, language: dv.language, description: dv.description, instruction: dv.instruction, sponsor_instruction: dv.sponsor_instruction}] AS descriptions, +[(concept_value)-[:HAS_TRANSLATED_TEXT]->(dv:OdmTranslatedText) | {text_type: dv.text_type, language: dv.language, text: dv.text}] AS translated_texts, [(concept_value)-[:HAS_ALIAS]->(av:OdmAlias) | {name: av.name, context: av.context}] AS aliases, [(concept_value)-[hud:HAS_UNIT_DEFINITION]->(udr:UnitDefinitionRoot)-[:LATEST]->(udv:UnitDefinitionValue) | {uid: udr.uid, name: udv.name, mandatory: hud.mandatory, order: hud.order}] AS unit_definitions, -head([(concept_value)-[:HAS_CODELIST]->(ctcr:CTCodelistRoot)-[:HAS_ATTRIBUTES_ROOT]-> -(:CTCodelistAttributesRoot)-[:LATEST]->(ctcav:CTCodelistAttributesValue) | ctcr.uid]) AS codelist_uid, +head([(concept_value)-[hc:HAS_CODELIST]->(ctcr:CTCodelistRoot)-[:HAS_ATTRIBUTES_ROOT]-> +(:CTCodelistAttributesRoot)-[:LATEST]->(ctcav:CTCodelistAttributesValue) | {uid: ctcr.uid, allows_multi_choice: hc.allows_multi_choice}]) AS codelist, [(concept_value)-[hct:HAS_CODELIST_TERM]->(:CTTermContext)-[:HAS_SELECTED_TERM]-> (cttr:CTTermRoot)-[:HAS_NAME_ROOT]->(cttnr:CTTermNameRoot)-[:LATEST]->(cttnv:CTTermNameValue) | -{uid: cttr.uid, name: cttnv.name, mandatory: hct.mandatory, order: hct.order}] AS terms, +{uid: cttr.uid, name: cttnv.name, display_text: hct.display_text, mandatory: hct.mandatory, order: hct.order}] AS terms, [(concept_value)-[hve:HAS_VENDOR_ELEMENT]->(vev:OdmVendorElementValue)<-[:HAS_VERSION]-(ver:OdmVendorElementRoot) | {uid: ver.uid, name: vev.name, value: hve.value}] AS vendor_elements, @@ -257,9 +273,12 @@ def specific_alias_clause(self, **kwargs) -> str: 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) + MATCH (ai)<-[:CONTAINS_ACTIVITY_ITEM]-(aiv:ActivityInstanceValue)<-[:LATEST]-(air:ActivityInstanceRoot) RETURN COLLECT(DISTINCT { activity_instance_uid: air.uid, + activity_instance_name: aiv.name, + activity_instance_adam_param_code: aiv.adam_param_code, + activity_instance_topic_code: aiv.topic_code, activity_item_class_uid: aicr.uid, odm_form_uid: ofr.uid, odm_item_group_uid: oigr.uid, @@ -289,13 +308,16 @@ def _get_or_create_value( self.manage_vendor_relationships( current_latest, new_value, ar.should_disconnect_relationships ) - self.connect_descriptions(ar.concept_vo.descriptions, new_value) + self.connect_translated_texts(ar.concept_vo.translated_texts, new_value) self.connect_aliases(ar.concept_vo.aliases, new_value) new_value.has_codelist.disconnect_all() - if ar.concept_vo.codelist_uid is not None: - codelist = CTCodelistRoot.nodes.get_or_none(uid=ar.concept_vo.codelist_uid) - new_value.has_codelist.connect(codelist) + if ar.concept_vo.codelist is not None: + codelist = CTCodelistRoot.nodes.get_or_none(uid=ar.concept_vo.codelist.uid) + new_value.has_codelist.connect( + codelist, + {"allows_multi_choice": ar.concept_vo.codelist.allows_multi_choice}, + ) for activity_instance in ar.concept_vo.activity_instances: db.cypher_query( @@ -357,15 +379,13 @@ def _create_new_value_node(self, ar: OdmItemAR) -> OdmItemValue: def _has_data_changed(self, ar: OdmItemAR, value: OdmItemValue) -> bool: are_concept_properties_changed = super()._has_data_changed(ar=ar, value=value) - description_nodes = { - OdmDescriptionModel( - name=description_node.name, - language=description_node.language, - description=description_node.description, - instruction=description_node.instruction, - sponsor_instruction=description_node.sponsor_instruction, + translated_text_nodes = { + OdmTranslatedTextModel( + text_type=translated_text_node.text_type, + language=translated_text_node.language, + text=translated_text_node.text, ) - for description_node in value.has_description.all() + for translated_text_node in value.has_translated_text.all() } alias_nodes = { OdmAliasModel(name=alias_node.name, context=alias_node.context) @@ -374,9 +394,12 @@ def _has_data_changed(self, ar: OdmItemAR, value: OdmItemValue) -> bool: unit_definition_uids = { unit_definition.uid for unit_definition in value.has_unit_definition.all() } - codelist_uid = ( - codelist.uid if (codelist := value.has_codelist.get_or_none()) else None - ) + if codelist := value.has_codelist.get_or_none(): + rel = value.has_codelist.relationship(codelist) + codelist = (codelist.uid, rel.allows_multi_choice or False) + else: + codelist = None + term_uids = { term.uid for term_context in value.has_codelist_term.all() @@ -425,10 +448,14 @@ def _has_data_changed(self, ar: OdmItemAR, value: OdmItemValue) -> bool: ] are_rels_changed = ( - set(ar.concept_vo.descriptions) != description_nodes + set(ar.concept_vo.translated_texts) != translated_text_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 ( + getattr(ar.concept_vo.codelist, "uid", None), + getattr(ar.concept_vo.codelist, "allows_multi_choice", None), + ) + != codelist or set(ar.concept_vo.term_uids) != term_uids or sorted(ar_activity_instances) != sorted(activity_instances) ) @@ -616,7 +643,7 @@ def find_unit_definition_with_item_relation_by_item_uid( def remove_all_codelist_terms_from_item(self, item_uid: str): db.cypher_query( """ - MATCH (:OdmItemRoot {uid: $uid})-[r:HAS_CODELIST_TERM]->(:CTTermContext) + MATCH (:OdmItemRoot {uid: $uid})-[:LATEST]->(:OdmItemValue)-[r:HAS_CODELIST_TERM]->(:CTTermContext) DELETE r """, params={"uid": item_uid}, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/method_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/method_repository.py index cd715790..1db51622 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/method_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/method_repository.py @@ -23,8 +23,8 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmFormalExpressionModel, + OdmTranslatedTextModel, ) from clinical_mdr_api.models.concepts.odms.odm_method import OdmMethod from common.utils import convert_to_datetime @@ -56,15 +56,13 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( ) for formal_expression_value in value.has_formal_expression.all() ], - descriptions=[ - OdmDescriptionModel( - name=description_value.name, - language=description_value.language, - description=description_value.description, - instruction=description_value.instruction, - sponsor_instruction=description_value.sponsor_instruction, + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text_value.text_type, + language=translated_text_value.language, + text=translated_text_value.text, ) - for description_value in value.has_description.all() + for translated_text_value in value.has_translated_text.all() ], aliases=[ OdmAliasModel(name=alias_value.name, context=alias_value.context) @@ -95,17 +93,13 @@ def _create_aggregate_root_instance_from_cypher_result( ) for formal_expression in input_dict["formal_expressions"] ], - descriptions=[ - OdmDescriptionModel( - name=description["name"], - language=description.get("language", None), - description=description.get("description", None), - instruction=description.get("instruction", None), - sponsor_instruction=description.get( - "sponsor_instruction", None - ), + translated_texts=[ + OdmTranslatedTextModel( + text_type=translated_text["text_type"], + language=translated_text["language"], + text=translated_text["text"], ) - for description in input_dict["descriptions"] + for translated_text in input_dict["translated_texts"] ], aliases=[ OdmAliasModel(name=alias["name"], context=alias["context"]) @@ -140,8 +134,7 @@ def specific_alias_clause(self, **kwargs) -> str: [(concept_value)-[:HAS_FORMAL_EXPRESSION]->(fev:OdmFormalExpression) | {context: fev.context, expression: fev.expression}] AS formal_expressions, -[(concept_value)-[:HAS_DESCRIPTION]->(dv:OdmDescription) | -{name: dv.name, language: dv.language, description: dv.description, instruction: dv.instruction, sponsor_instruction: dv.sponsor_instruction}] AS descriptions, +[(concept_value)-[:HAS_TRANSLATED_TEXT]->(dv:OdmTranslatedText) | {text_type: dv.text_type, language: dv.language, text: dv.text}] AS translated_texts, [(concept_value)-[:HAS_ALIAS]->(av:OdmAlias) | {name: av.name, context: av.context}] AS aliases """ @@ -152,7 +145,7 @@ def _get_or_create_value( new_value = super()._get_or_create_value(root, ar, force_new_value_node) self.connect_aliases(ar.concept_vo.aliases, new_value) - self.connect_descriptions(ar.concept_vo.descriptions, new_value) + self.connect_translated_texts(ar.concept_vo.translated_texts, new_value) self.connect_formal_expressions(ar.concept_vo.formal_expressions, new_value) return new_value @@ -177,15 +170,13 @@ def _has_data_changed(self, ar: OdmMethodAR, value: OdmMethodValue) -> bool: ) for formal_expression_node in value.has_formal_expression.all() } - description_nodes = { - OdmDescriptionModel( - name=description_node.name, - language=description_node.language, - description=description_node.description, - instruction=description_node.instruction, - sponsor_instruction=description_node.sponsor_instruction, + translated_text_nodes = { + OdmTranslatedTextModel( + text_type=translated_text_node.text_type, + language=translated_text_node.language, + text=translated_text_node.text, ) - for description_node in value.has_description.all() + for translated_text_node in value.has_translated_text.all() } alias_nodes = { OdmAliasModel(name=alias_node.name, context=alias_node.context) @@ -194,7 +185,7 @@ def _has_data_changed(self, ar: OdmMethodAR, value: OdmMethodValue) -> bool: are_rels_changed = ( set(ar.concept_vo.formal_expressions) != formal_expression_nodes - or set(ar.concept_vo.descriptions) != description_nodes + or set(ar.concept_vo.translated_texts) != translated_text_nodes or set(ar.concept_vo.aliases) != alias_nodes ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/odm_generic_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/odm_generic_repository.py index 84886c38..c1b920d0 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/odm_generic_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/odm_generic_repository.py @@ -16,7 +16,6 @@ from clinical_mdr_api.domain_repositories.models.generic import VersionValue from clinical_mdr_api.domain_repositories.models.odm import ( OdmAlias, - OdmDescription, OdmFormalExpression, OdmFormRoot, OdmFormValue, @@ -24,22 +23,25 @@ OdmItemGroupValue, OdmItemRoot, OdmItemValue, + OdmTranslatedText, OdmVendorAttributeRoot, OdmVendorAttributeValue, OdmVendorElementRoot, OdmVendorElementValue, ) from clinical_mdr_api.domains.concepts.utils import RelationType +from clinical_mdr_api.domains.enums import OdmTranslatedTextTypeEnum from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmElementWithParentUid, OdmFormalExpressionModel, + OdmTranslatedTextModel, ) from clinical_mdr_api.repositories._utils import ( CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, sb_clear_cache, ) from common.exceptions import BusinessLogicException @@ -106,13 +108,14 @@ def find_all( extracted_items = self._retrieve_concepts_from_cypher_res( result_array, attributes_names ) - - 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 + total_amount = calculate_total_count_from_query_result( + len(extracted_items), page_number, page_size, total_count ) + if total_amount is None: + count_result, _ = db.cypher_query( + query=query.count_query, params=query.parameters + ) + total_amount = count_result[0][0] if len(count_result) > 0 else 0 return extracted_items, total_amount @@ -403,34 +406,23 @@ def odm_object_exists( return None - def connect_descriptions( - self, descriptions: list[OdmDescriptionModel], new_value: VersionValue + def connect_translated_texts( + self, translated_texts: list[OdmTranslatedTextModel], new_value: VersionValue ): - new_value.has_description.disconnect_all() + new_value.has_translated_text.disconnect_all() - for description in descriptions: - params: dict[str, Any] = { - "name": description.name, - "language": description.language, + for translated_text in translated_texts: + params: dict[str, str | OdmTranslatedTextTypeEnum] = { + "text": translated_text.text, + "language": translated_text.language, + "text_type": translated_text.text_type.value, } - for attr in ["description", "instruction", "sponsor_instruction"]: - value = getattr(description, attr) - if value is not None: - params[attr] = value - else: - params[f"{attr}__isnull"] = True - - description_node = OdmDescription.nodes.get_or_none(**params) - if not description_node: - description_node = OdmDescription( - name=description.name, - language=description.language, - description=description.description, - instruction=description.instruction, - sponsor_instruction=description.sponsor_instruction, - ) - description_node.save() - new_value.has_description.connect(description_node) + + translated_text_node = OdmTranslatedText.nodes.get_or_none(**params) + if not translated_text_node: + translated_text_node = OdmTranslatedText(**params) + translated_text_node.save() + new_value.has_translated_text.connect(translated_text_node) def connect_aliases(self, aliases: list[OdmAliasModel], new_value: VersionValue): new_value.has_alias.disconnect_all() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_attribute_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_attribute_repository.py index fdd6265b..e1d72220 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_attribute_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_attribute_repository.py @@ -33,6 +33,7 @@ from clinical_mdr_api.models.concepts.odms.odm_vendor_attribute import ( OdmVendorAttribute, ) +from clinical_mdr_api.services._utils import ensure_transaction from common.exceptions import BusinessLogicException from common.utils import convert_to_datetime @@ -290,3 +291,61 @@ def find_by_uid_with_odm_element_relation( value=rs[0][4], vendor_namespace_uid=rs[0][5], ) + + @ensure_transaction(db) + def _connect_relationships_to_new_value_node( + self, root: VersionRoot, _: VersionValue + ) -> None: + """ + - Upgrades all incoming HAS_VENDOR_ELEMENT_ATTRIBUTE relationships to the second latest version to point + to the latest version of OdmVendorAttributeValue, preserving relationship properties. + - Upgrades all incoming HAS_VENDOR_ATTRIBUTE relationships to the second latest version to point + to the latest version of OdmVendorAttributeValue, preserving relationship properties. + """ + 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, 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|OdmItemGroupRoot|OdmItemRoot)-[p_ver_rel:HAS_VERSION]-> + (parent_value:OdmFormValue|OdmItemGroupValue|OdmItemValue)-[has_vendor_element_attribute:HAS_VENDOR_ELEMENT_ATTRIBUTE]->(second_latest_value) + WHERE p_ver_rel.end_date IS NULL AND p_ver_rel.status = "Draft" + + WITH latest_value, has_vendor_element_attribute, parent_value, + has_vendor_element_attribute.value AS value + + CREATE (parent_value)-[new_has_vendor_element_attribute:HAS_VENDOR_ELEMENT_ATTRIBUTE]->(latest_value) + + SET new_has_vendor_element_attribute.value = value + + DELETE has_vendor_element_attribute + """ + db.cypher_query(query, {"root_uid": root.uid}) + + 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, 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|OdmItemGroupRoot|OdmItemRoot)-[p_ver_rel:HAS_VERSION]-> + (parent_value:OdmFormValue|OdmItemGroupValue|OdmItemValue)-[has_vendor_attribute:HAS_VENDOR_ATTRIBUTE]->(second_latest_value) + WHERE p_ver_rel.end_date IS NULL AND p_ver_rel.status = "Draft" + + WITH latest_value, has_vendor_attribute, parent_value, + has_vendor_attribute.value AS value + + CREATE (parent_value)-[new_has_vendor_attribute:HAS_VENDOR_ATTRIBUTE]->(latest_value) + + SET new_has_vendor_attribute.value = value + + DELETE has_vendor_attribute + """ + db.cypher_query(query, {"root_uid": root.uid}) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_element_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_element_repository.py index 9e1452de..36b0fb38 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_element_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/vendor_element_repository.py @@ -29,6 +29,7 @@ LibraryVO, ) from clinical_mdr_api.models.concepts.odms.odm_vendor_element import OdmVendorElement +from clinical_mdr_api.services._utils import ensure_transaction from common.exceptions import BusinessLogicException from common.utils import convert_to_datetime @@ -120,6 +121,18 @@ def _get_or_create_value( ar: OdmVendorElementAR, force_new_value_node: bool = False, ) -> VersionValue: + current_latest = root.has_latest_value.single() + old_has_vendor_attribute_nodes = ( + current_latest.has_vendor_attribute.all() if current_latest else [] + ) + new_has_vendor_attribute_nodes = [ + old_vendor_attribute_root.has_latest_value.single() + for old_has_vendor_attribute_node in old_has_vendor_attribute_nodes + if ( + old_vendor_attribute_root := old_has_vendor_attribute_node.has_root.single() + ) + ] + new_value = super()._get_or_create_value(root, ar, force_new_value_node) new_value.belongs_to_vendor_namespace.disconnect_all() @@ -134,6 +147,15 @@ def _get_or_create_value( if vendor_namespace_value: new_value.belongs_to_vendor_namespace.connect(vendor_namespace_value) + for new_has_vendor_attribute_node in new_has_vendor_attribute_nodes: + new_value.has_vendor_attribute.connect(new_has_vendor_attribute_node) + + if ar.should_disconnect_relationships: + for old_has_vendor_attribute_node in old_has_vendor_attribute_nodes: + current_latest.has_vendor_attribute.disconnect( + old_has_vendor_attribute_node + ) + return new_value def _create_new_value_node(self, ar: OdmVendorElementAR) -> OdmVendorElementValue: @@ -207,3 +229,35 @@ def find_by_uid_with_odm_element_relation( compatible_types=rs[0][1], value=rs[0][2], ) + + @ensure_transaction(db) + def _connect_relationships_to_new_value_node( + self, root: VersionRoot, _: VersionValue + ) -> None: + """ + Upgrades all incoming HAS_VENDOR_ELEMENT relationships to the second latest version to point + to the latest version of OdmVendorElementValue, preserving relationship properties. + """ + 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, 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|OdmItemGroupRoot|OdmItemRoot)-[p_ver_rel:HAS_VERSION]-> + (parent_value:OdmFormValue|OdmItemGroupValue|OdmItemValue)-[has_vendor_element:HAS_VENDOR_ELEMENT]->(second_latest_value) + WHERE p_ver_rel.end_date IS NULL AND p_ver_rel.status = "Draft" + + WITH latest_value, has_vendor_element, parent_value, + has_vendor_element.value AS value + + CREATE (parent_value)-[new_has_vendor_element:HAS_VENDOR_ELEMENT]->(latest_value) + + SET new_has_vendor_element.value = value + + DELETE has_vendor_element + """ + db.cypher_query(query, {"root_uid": root.uid}) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_aggregated_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_aggregated_repository.py index cb3b6b3c..521dc84e 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_aggregated_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_aggregated_repository.py @@ -30,6 +30,8 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, + validate_filter_by_dict, validate_filters_and_add_search_string, ) from common.exceptions import BusinessLogicException, ValidationException @@ -146,6 +148,62 @@ def _create_codelist_aggregate_instances_from_cypher_result( ) return codelist_name_ar, codelist_attributes_ar, paired_codelists + def minimal_count_query( + self, + catalogue_name: str | None, + library: str | None, + package: str | None, + is_sponsor: bool, + filter_by: dict[str, dict[str, Any]] | None, + term_filter: dict[str, str | list[Any]] | None, + ) -> tuple[str | None, dict[str, Any]]: + + # if library_name is included in the filters, with a single value, + # remove it and instead use the library parameter + if filter_by and "library_name" in filter_by and not library: + library_filter: list[str] = filter_by["library_name"]["v"] + if len(library_filter) == 1 and library_filter[0] in ("CDISC", "Sponsor"): + library = library_filter[0] + del filter_by["library_name"] + + filter_by = validate_filter_by_dict(filter_by) + # if filters are provided, no minimal query can be made + if filter_by is not None and len(filter_by) > 0: + return None, {} + # if term filters are provided, no minimal query can be made + if term_filter is not None and len(term_filter) > 0: + return None, {} + + where_clauses = [] + params = {} + if catalogue_name: + where_clauses.append( + "(codelist_root)<-[:HAS_CODELIST]-(:CTCatalogue {name: $catalogue_name})" + ) + params["catalogue_name"] = catalogue_name + if library: + where_clauses.append( + "(:Library {name: $library})-[:CONTAINS_CODELIST]->(codelist_root)" + ) + params["library"] = library + if package: + where_clauses.append( + """ + (:CTPackage {name: $package})-[:CONTAINS_CODELIST]->(:CTPackageCodelist)-[:CONTAINS_ATTRIBUTES]-> + (:CTCodelistAttributesValue)<-[:HAS_VERSION]-(:CTCodelistAttributesRoot)<-[:HAS_ATTRIBUTES_ROOT]-(codelist_root) + """ + ) + params["package"] = package + if is_sponsor: + where_clauses.append( + "(:Library {name: 'Sponsor'})-[:CONTAINS_CODELIST]->(codelist_root)" + ) + query = "MATCH (codelist_root:CTCodelistRoot)" + if where_clauses: + query += " WHERE " + " AND ".join(where_clauses) + query += " RETURN count(DISTINCT codelist_root) AS count" + return query, params + def find_all_aggregated_result( self, catalogue_name: str | None = None, @@ -204,6 +262,16 @@ def find_all_aggregated_result( self.sponsor_alias_clause if is_sponsor else self.generic_alias_clause ) + minimal_count_query, minimal_count_params = self.minimal_count_query( + catalogue_name=catalogue_name, + library=library, + package=package, + is_sponsor=is_sponsor, + filter_by=filter_by, + term_filter=term_filter, + ) + filtering_active = minimal_count_query is None + query = CypherQueryBuilder( match_clause=match_clause, alias_clause=alias_clause, @@ -215,6 +283,7 @@ def find_all_aggregated_result( total_count=total_count, wildcard_properties_list=list_codelist_wildcard_properties(), format_filter_sort_keys=format_codelist_filter_sort_keys, + one_element_extra=filtering_active, ) query.parameters.update(filter_query_parameters) @@ -231,13 +300,20 @@ def find_all_aggregated_result( ) ) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(codelists_ars), + page_number, + page_size, + total_count, + extra_requested=filtering_active, + ) + if 0 < page_size < len(codelists_ars): + codelists_ars = codelists_ars[:page_size] + if total is None: count_result, _ = db.cypher_query( - query=query.count_query, params=query.parameters + query=minimal_count_query, params=minimal_count_params ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return codelists_ars, total @@ -574,6 +650,7 @@ def find_all_terms_aggregated_result( ct_term_root.uid AS term_uid, ct_cl_term.submission_value AS submission_value, ht.order AS order, + ht.ordinal AS ordinal, ht.start_date AS start_date, ht.end_date AS end_date, tav.definition AS definition, @@ -629,13 +706,14 @@ def find_all_terms_aggregated_result( term_dictionary[attribute_name] = term_property codelist_term_ars.append(CTCodelistTermAR.from_result_dict(term_dictionary)) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(codelist_term_ars), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return codelist_term_ars, total @@ -744,6 +822,7 @@ def get_distinct_term_headers( ct_term_root.uid AS term_uid, ct_cl_term.submission_value AS submission_value, ht.order AS order, + ht.ordinal AS ordinal, ht.start_date AS start_date, ht.end_date AS end_date, tav.definition AS definition, 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 70073c3c..6aa0fca2 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 @@ -89,7 +89,7 @@ def _create_aggregate_root_instance_from_cypher_result( preferred_term=codelist_dict.get("value_node").get("preferred_term"), definition=codelist_dict.get("value_node").get("definition"), extensible=codelist_dict.get("value_node").get("extensible"), - ordinal=bool(codelist_dict.get("value_node").get("ordinal")), + is_ordinal=bool(codelist_dict.get("value_node").get("is_ordinal")), ), library=LibraryVO.from_input_values_2( library_name=codelist_dict["library_name"], @@ -139,7 +139,7 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( preferred_term=value.preferred_term, definition=value.definition, extensible=value.extensible, - ordinal=value.ordinal, + is_ordinal=value.is_ordinal, ), library=LibraryVO.from_input_values_2( library_name=library.name, @@ -166,7 +166,7 @@ def _get_or_create_value( preferred_term=ar.ct_codelist_vo.preferred_term, definition=ar.ct_codelist_vo.definition, extensible=ar.ct_codelist_vo.extensible, - ordinal=ar.ct_codelist_vo.ordinal, + is_ordinal=ar.ct_codelist_vo.is_ordinal, ): return itm latest_draft = root.latest_draft.get_or_none() @@ -185,7 +185,7 @@ def _get_or_create_value( preferred_term=ar.ct_codelist_vo.preferred_term, definition=ar.ct_codelist_vo.definition, extensible=ar.ct_codelist_vo.extensible, - ordinal=ar.ct_codelist_vo.ordinal, + is_ordinal=ar.ct_codelist_vo.is_ordinal, ) self._db_save_node(new_value) return new_value @@ -197,7 +197,7 @@ def _has_data_changed(self, ar: CTCodelistAttributesAR, value: VersionValue): or ar.ct_codelist_vo.preferred_term != value.preferred_term or ar.ct_codelist_vo.definition != value.definition or ar.ct_codelist_vo.extensible != value.extensible - or ar.ct_codelist_vo.ordinal != value.ordinal + or ar.ct_codelist_vo.is_ordinal != value.is_ordinal ) def _create(self, item: CTCodelistAttributesAR) -> CTCodelistAttributesAR: @@ -215,7 +215,7 @@ def _create(self, item: CTCodelistAttributesAR) -> CTCodelistAttributesAR: preferred_term=item.ct_codelist_vo.preferred_term, definition=item.ct_codelist_vo.definition, extensible=item.ct_codelist_vo.extensible, - ordinal=item.ct_codelist_vo.ordinal, + is_ordinal=item.ct_codelist_vo.is_ordinal, ) self._db_save_node(root) 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 eaeb951b..0c66e578 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 @@ -51,6 +51,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, sb_clear_cache, validate_filters_and_add_search_string, ) @@ -228,13 +229,14 @@ def find_all( result_array, attributes_names ) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(extracted_items), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return GenericFilteringReturn(items=extracted_items, total=total) @@ -438,6 +440,7 @@ def add_term( author_id: str, order: int, submission_value: str, + ordinal: float | None = None, ) -> None: """ Method adds term identified by term_uid to the codelist identified by codelist_uid. @@ -512,6 +515,7 @@ def add_term( "end_date": None, "author_id": author_id, "order": order, + "ordinal": ordinal, }, ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_get_all_query_utils.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_get_all_query_utils.py index 2f233257..e381c44b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_get_all_query_utils.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_get_all_query_utils.py @@ -259,6 +259,7 @@ def create_term_codelist_vos_from_cypher_result(term_dict: dict[str, Any]) -> CT codelist_uid=cl["codelist_uid"], submission_value=cl["submission_value"], order=cl["order"], + ordinal=cl.get("ordinal"), library_name=cl["library_name"], codelist_name=cl["codelist_name"], codelist_submission_value=cl["codelist_submission_value"], @@ -501,8 +502,8 @@ def create_codelist_attributes_aggregate_instances_from_cypher_result( extensible=codelist_dict.get(f"value_node{specific_suffix}").get( "extensible" ), - ordinal=bool( - codelist_dict.get(f"value_node{specific_suffix}").get("ordinal") + is_ordinal=bool( + codelist_dict.get(f"value_node{specific_suffix}").get("is_ordinal") ), ), library=LibraryVO.from_input_values_2( @@ -553,7 +554,7 @@ def format_codelist_filter_sort_keys(key: str, prefix: str | None = None) -> str ) if key == "template_parameter": return "is_template_parameter" - if key in ["name", "definition", "submission_value", "extensible", "ordinal"]: + if key in ["name", "definition", "submission_value", "extensible", "is_ordinal"]: return f"value_node_{prefix}.{key}" if prefix else f"value_node.{key}" # Property coming from relationship if key in [ diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_package_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_package_repository.py index 98e34558..4362abdc 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_package_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_package_repository.py @@ -199,6 +199,15 @@ def create_sponsor_package( sponsor_package_uid = ( f"Sponsor {catalogue_node.name} {effective_date.strftime('%Y-%m-%d')}" ) + + # Pre-creation validation: check if a package with the same name already exists + # This provides an additional safety layer beyond the database constraint + existing_package = CTPackage.nodes.get_or_none(name=sponsor_package_uid) + if existing_package is not None: + raise AlreadyExistsException( + msg="A sponsor CTPackage already exists for this date" + ) + sponsor_package = CTPackage( uid=sponsor_package_uid, name=sponsor_package_uid, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_aggregated_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_aggregated_repository.py index ddef4b17..6ff9f480 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_aggregated_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_aggregated_repository.py @@ -34,6 +34,8 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, + validate_filter_by_dict, validate_filters_and_add_search_string, ) from common.exceptions import ValidationException @@ -255,6 +257,16 @@ def find_all_aggregated_result( include_removed_terms=include_removed_terms, ) + filter_by = validate_filter_by_dict(filter_by) + filtering_active = ( + filter_by is not None + and len(filter_by) > 0 + or library is not None + or package is not None + or codelist_name is not None + or codelist_uid is not None + ) + # Build alias_clause alias_clause = ( self.sponsor_alias_clause(package=package) @@ -269,6 +281,7 @@ def find_all_aggregated_result( implicit_sort_by="term_uid", page_number=page_number, page_size=page_size, + one_element_extra=filtering_active, filter_by=FilterDict.model_validate({"elements": filter_by}), filter_operator=filter_operator, total_count=total_count, @@ -291,14 +304,22 @@ def find_all_aggregated_result( ) ) - total = 0 - if total_count: - count_result, _ = db.cypher_query( - query=query.count_query, params=query.parameters - ) - if len(count_result) > 0: - total = count_result[0][0] - + total = calculate_total_count_from_query_result( + len(terms_ars), + page_number, + page_size, + total_count, + extra_requested=filtering_active, + ) + if 0 < page_size < len(terms_ars): + terms_ars = terms_ars[:page_size] + if total is None: + if not filtering_active: + count_query = "MATCH (term_root:CTTermRoot) RETURN count(term_root) AS total_count" + count_result, _ = db.cypher_query(query=count_query) + total = count_result[0][0] if len(count_result) > 0 else 0 + else: + total = -1 return terms_ars, total def get_distinct_headers( @@ -751,13 +772,14 @@ def find_term_codelists( """ alias_clause = """ DISTINCT term_root, codelist_term, codelist_root, codelist_name_value, codelist_attributes_value, codelist_library, ht - WITH + WITH codelist_root.uid AS codelist_uid, codelist_name_value.name AS codelist_name, codelist_attributes_value.submission_value AS codelist_submission_value, codelist_attributes_value.concept_id AS codelist_concept_id, codelist_term.submission_value AS term_submission_value, ht.order AS term_order, + ht.ordinal AS term_ordinal, ht.start_date AS term_start_date, codelist_library.name AS library_name """ @@ -790,6 +812,7 @@ def _create_term_codelists_from_cypher_result( codelist_uid=cl_dictionary["codelist_uid"], submission_value=cl_dictionary["term_submission_value"], order=cl_dictionary["term_order"], + ordinal=cl_dictionary.get("term_ordinal"), library_name=cl_dictionary["library_name"], codelist_name=cl_dictionary["codelist_name"], codelist_submission_value=cl_dictionary[ 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 7ba6b7b0..c66f094d 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 @@ -40,6 +40,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, sb_clear_cache, validate_filters_and_add_search_string, ) @@ -214,6 +215,28 @@ def get_term_attributes_by_term_uids(self, term_uids: list[Any]): return items, prop_names + def get_codelist_to_term_properties(self, term_uid: str, codelist_uid: str): + query = """ + MATCH (codelist_root {uid: $codelist_uid})-[ht:HAS_TERM]->(cct:CTCodelistTerm)-[:HAS_TERM_ROOT]->(:CTTermRoot {uid: $term_uid}) + RETURN + ht.start_date AS start_date, + ht.end_date AS end_date, + ht.order AS order, + ht.ordinal AS ordinal, + cct.submission_value AS submission_value + ORDER BY ht.start_date DESC + LIMIT 1 + """ + + items, prop_names = db.cypher_query( + query, {"term_uid": term_uid, "codelist_uid": codelist_uid} + ) + + if not items or not items[0]: + return {} + + return dict(zip(prop_names, items[0])) + def find_all( self, codelist_uid: str | None = None, @@ -290,13 +313,14 @@ def find_all( result_array, attributes_names ) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(extracted_items), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return GenericFilteringReturn(items=extracted_items, total=total) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_codelist_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_codelist_repository.py index 249b89fd..00dbe68d 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_codelist_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_codelist_repository.py @@ -44,6 +44,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, sb_clear_cache, validate_filters_and_add_search_string, ) @@ -247,12 +248,14 @@ def find_all( result_array, attributes_names ) - 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 + total_amount = calculate_total_count_from_query_result( + len(extracted_items), page_number, page_size, total_count ) + if total_amount is None: + count_result, _ = db.cypher_query( + query=query.count_query, params=query.parameters + ) + total_amount = count_result[0][0] if len(count_result) > 0 else 0 return extracted_items, total_amount diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_repository.py index 6a3dd19a..e4c6aa58 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_repository.py @@ -46,6 +46,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, sb_clear_cache, validate_filters_and_add_search_string, ) @@ -251,12 +252,14 @@ def find_all( result_array, attributes_names ) - 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 + total_amount = calculate_total_count_from_query_result( + len(extracted_items), page_number, page_size, total_count ) + if total_amount is None: + count_result, _ = db.cypher_query( + query=query.count_query, params=query.parameters + ) + total_amount = count_result[0][0] if len(count_result) > 0 else 0 return extracted_items, total_amount diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_substance_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_substance_repository.py index 165d1cff..ff559f6a 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_substance_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/dictionaries/dictionary_term_substance_repository.py @@ -32,6 +32,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, validate_filters_and_add_search_string, ) from clinical_mdr_api.services.user_info import UserInfoService @@ -185,12 +186,14 @@ def find_all( result_array, attributes_names ) - 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 + total_amount = calculate_total_count_from_query_result( + len(extracted_items), page_number, page_size, total_count ) + if total_amount is None: + count_result, _ = db.cypher_query( + query=query.count_query, params=query.parameters + ) + total_amount = count_result[0][0] if len(count_result) > 0 else 0 return extracted_items, total_amount 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 0fe045de..420bac02 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 @@ -18,8 +18,6 @@ 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 @@ -223,98 +221,6 @@ 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, - new_item: Any, - exclude_study_selection_relationships: list[Any] | None = None, -): - """ - Method for preserving the previous version's connected StudySelection(s) relationships to the current version. - Take into account that the StudySelection(s) that will be kept are only those - - those StudySelections that are linked to the study_value_node supplied as a parameter ":param study_value_node:" - - those StudySelections that are specified as relationship on the NeoModel Class object - - It is possible to exclude StudySelection(s) if they are already kept and can be connected and found by UID on the VO. - By giving the parameter ":param exclude_study_selection_relationships:" the StudySelections will be excluded. - This method's purpose is to be maintenance-driven (constantly maintain and define what will be omitted). - - :param previous_item: Any, Previous item from which relationships should be maintained - :param study_value_node: Any, StudyValue node from which the previous item should be disconnected - :param new_item: Any, New item to link the existing relationships - :param exclude_relationships: list[Union[list[Union[str,Any]],Any]] = None, - Excluded relationships to keep because they are maintained (linked) by its uid - * There are two ways to define exclusion: - * list[Type[StudySelectionNeoModel]: type of the node] - * list[(str: relationship_name, Type[StudySelectionNeoModel]: type of the node )] - * For instance: - * we can define either simply the node type object on exclude_relationships --> [CTTermRoot,...] - * or we can define the specific relationship exclude_relationships --> [("has_visit_contact_mode", CTTermRoot),...], - * Both might also be in the same List exclude_relationships --> [("has_visit_contact_mode", CTTermRoot), UnitDefinitionRoot, ...] - - :raises: BusinessLogicException -- An exception is thrown if the previous node is not connected to a StudyValue, - to ensure that the relationships are preserved. - - :return: - """ - if not exclude_study_selection_relationships: - exclude_study_selection_relationships = [] - # ensure that StudyValue will be excluded from being maintained, later will be dropped - exclude_study_selection_relationships.append(type(study_value_node)) - exclude_study_selection_relationships.append(StudyAction) - study_selection_relationships = [ - (rel[0], rel[1].definition["node_class"]) - for rel in previous_item.__all_relationships__ - if ( - issubclass( - rel[1].definition["node_class"], - (StudySelection, StudyField, type(study_value_node), StudyAction), - ) - ) - ] - study_value_rel_name = None - for rel_name, target_node_type in study_selection_relationships: - if target_node_type == type(study_value_node): - study_value_rel_name = rel_name - - study_action_rels = [ - i_rel for i_rel in study_selection_relationships if i_rel[1] == StudyAction - ] - # filter just those relationships that we want to maintain, to not appear if rel in exclude_study_selection_relationships - relationships_to_maintain = [ - i_rel - for i_rel in study_selection_relationships - if not ( - i_rel in exclude_study_selection_relationships - or i_rel[1] in exclude_study_selection_relationships - ) - ] - # MAINTAIN non filtered relationships, just for those non filtered relationships nodes with StudyValue connection - for connected_rel_name, _ in relationships_to_maintain: - connected_nodes = get_connected_node_by_rel_name_and_study_value( - node=previous_item, - connected_rel_name=connected_rel_name, - study_value=study_value_node, - multiple_returned_nodes=True, - at_least_one_returned=False, - ) - # connect to those connected nodes with same study_value as new_item - for i_connected_node in connected_nodes: - getattr(new_item, connected_rel_name).connect(i_connected_node) - # run ".single()" to confirm that the StudyAction cardinalities are correct. - for study_action_rel_name, _ in study_action_rels: - getattr(previous_item, study_action_rel_name).single() - getattr(new_item, study_action_rel_name).single() - # DROP StudyValue relationship - if study_value_rel_name: - ValidationException.raise_if_not( - getattr(previous_item, study_value_rel_name).single(), - 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, @@ -322,7 +228,7 @@ def _manage_versioning_with_relations( before: StructuredNode | None = None, after: StructuredNode | None = None, exclude_relationships: Iterable[ - type[StructuredNode] | type[RelationshipDefinition] | str + type[StructuredNode] | RelationshipDefinition | str ] = tuple(), **properties, ) -> StudyAction: @@ -408,10 +314,10 @@ def _manage_versioning_with_relations( for rel in exclude_relationships: if isinstance(rel, str): _exclude_relationships.add(rel) + elif isinstance(rel, RelationshipDefinition): + _exclude_relationships.add(rel.definition["relation_type"]) 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." 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 868c48e4..427e1952 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 @@ -2670,11 +2670,12 @@ def _activity_instance_root_match_return_stmt(self): }) AS odm_items } RETURN COLLECT( distinct { - activity_item_class_uid: activity_item_class_root.uid, - activity_item_class_name: activity_item_class_value.name, - ct_terms:ct_terms, + activity_item_class_uid: activity_item_class_root.uid, + activity_item_class_name: activity_item_class_value.name, + ct_terms:ct_terms, unit_definitions: unit_definitions, is_adam_param_specific: activity_item.is_adam_param_specific, + text_value: activity_item.text_value, odm_forms: odm_forms, odm_item_groups: odm_item_groups, odm_items: odm_items 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 165b2227..c0ee99ba 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 @@ -172,6 +172,7 @@ class ActivityRoot(ConceptRoot): class ActivityItem(ClinicalMdrNode): is_adam_param_specific = BooleanProperty(False) + text_value = StringProperty() has_activity_item_class = RelationshipFrom( ActivityItemClassRoot, "HAS_ACTIVITY_ITEM", 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 7e55974d..2c80f7dc 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 @@ -10,6 +10,7 @@ ) from clinical_mdr_api.domain_repositories.models.controlled_terminology import ( + CTCodelistRoot, CTTermContext, ) from clinical_mdr_api.domain_repositories.models.generic import ( @@ -121,3 +122,8 @@ class ActivityItemClassRoot(VersionRoot): "MAPS_VARIABLE_CLASS", model=ClinicalMdrRel, ) + has_valid_codelist_for_items = RelationshipTo( + CTCodelistRoot, + "HAS_VALID_CODELIST_FOR_ITEMS", + model=ClinicalMdrRel, + ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/controlled_terminology.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/controlled_terminology.py index 480ce872..288a3b57 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/controlled_terminology.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/controlled_terminology.py @@ -4,6 +4,7 @@ ArrayProperty, BooleanProperty, DateProperty, + FloatProperty, IntegerProperty, One, RelationshipFrom, @@ -57,7 +58,7 @@ class CTCodelistAttributesValue(ControlledTerminology): definition = StringProperty() extensible = BooleanProperty() synonyms = ArrayProperty() - ordinal = BooleanProperty(default=False) + is_ordinal = BooleanProperty(default=False) class CTCodelistAttributesRoot(ControlledTerminology): @@ -164,6 +165,7 @@ class CodelistTermRelationship(ClinicalMdrRel): end_date: datetime | None = ZonedDateTimeProperty() author_id = StringProperty() order: int | None = IntegerProperty() + ordinal: float | None = FloatProperty() class CTCodelistRoot(ControlledTerminologyWithUID): 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 bafcb077..87cc02b6 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 @@ -41,23 +41,25 @@ class OdmAlias(VersionValue): has_item = RelationshipFrom("OdmItemValue", "HAS_ALIAS", model=ClinicalMdrRel) -class OdmDescription(VersionValue): - name = StringProperty() +class OdmTranslatedText(VersionValue): + text_type = StringProperty() language = StringProperty() - description = StringProperty() - instruction = StringProperty() - sponsor_instruction = StringProperty() + text = StringProperty() - has_form = RelationshipFrom("OdmFormValue", "HAS_DESCRIPTION", model=ClinicalMdrRel) + has_form = RelationshipFrom( + "OdmFormValue", "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel + ) has_item_group = RelationshipFrom( - "OdmItemGroupValue", "HAS_DESCRIPTION", model=ClinicalMdrRel + "OdmItemGroupValue", "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel + ) + has_item = RelationshipFrom( + "OdmItemValue", "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) - has_item = RelationshipFrom("OdmItemValue", "HAS_DESCRIPTION", model=ClinicalMdrRel) has_condition = RelationshipFrom( - "OdmConditionValue", "HAS_DESCRIPTION", model=ClinicalMdrRel + "OdmConditionValue", "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) has_method = RelationshipFrom( - "OdmMethodValue", "HAS_DESCRIPTION", model=ClinicalMdrRel + "OdmMethodValue", "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) @@ -75,8 +77,8 @@ class OdmFormalExpression(VersionValue): class OdmConditionValue(ConceptValue): oid = StringProperty() - has_description = RelationshipTo( - OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel + has_translated_text = RelationshipTo( + OdmTranslatedText, "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) has_alias = RelationshipTo(OdmAlias, "HAS_ALIAS", model=ClinicalMdrRel) has_formal_expression = RelationshipTo( @@ -107,8 +109,8 @@ class OdmConditionRoot(ConceptRoot): class OdmMethodValue(ConceptValue): oid = StringProperty() method_type = StringProperty() - has_description = RelationshipTo( - OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel + has_translated_text = RelationshipTo( + OdmTranslatedText, "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) has_alias = RelationshipTo(OdmAlias, "HAS_ALIAS", model=ClinicalMdrRel) has_formal_expression = RelationshipTo( @@ -157,8 +159,8 @@ class OdmFormValue(ConceptValue): links_to_activity_item = RelationshipTo(ActivityItem, "LINKS_TO_ACTIVITY_ITEM") - has_description = RelationshipTo( - OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel + has_translated_text = RelationshipTo( + OdmTranslatedText, "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) has_alias = RelationshipTo(OdmAlias, "HAS_ALIAS", model=ClinicalMdrRel) @@ -218,8 +220,8 @@ class OdmItemGroupValue(ConceptValue): links_to_activity_item = RelationshipTo(ActivityItem, "LINKS_TO_ACTIVITY_ITEM") - has_description = RelationshipTo( - OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel + has_translated_text = RelationshipTo( + OdmTranslatedText, "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) has_alias = RelationshipTo(OdmAlias, "HAS_ALIAS", model=ClinicalMdrRel) @@ -284,6 +286,10 @@ class ActivityItemRel(ClinicalMdrRel): value_dependent_map = StringProperty() +class OdmItemCodelistRelationship(ClinicalMdrRel): + allows_multi_choice = BooleanProperty() + + class OdmItemValue(ConceptValue): oid = StringProperty() prompt = StringProperty() @@ -299,8 +305,8 @@ class OdmItemValue(ConceptValue): ActivityItem, "LINKS_TO_ACTIVITY_ITEM", model=ActivityItemRel ) - has_description = RelationshipTo( - OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel + has_translated_text = RelationshipTo( + OdmTranslatedText, "HAS_TRANSLATED_TEXT", model=ClinicalMdrRel ) has_alias = RelationshipTo(OdmAlias, "HAS_ALIAS", model=ClinicalMdrRel) @@ -311,7 +317,9 @@ class OdmItemValue(ConceptValue): "HAS_UNIT_DEFINITION", model=OdmItemUnitDefinitionRelationship, ) - has_codelist = RelationshipTo(CTCodelistRoot, "HAS_CODELIST", model=ClinicalMdrRel) + has_codelist = RelationshipTo( + CTCodelistRoot, "HAS_CODELIST", model=OdmItemCodelistRelationship + ) has_codelist_term = RelationshipTo( CTTermContext, "HAS_CODELIST_TERM", 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 fed30859..64b916c0 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 @@ -532,6 +532,7 @@ class StudyDesignCell(StudySelection): class StudyArm(StudySelection): name = StringProperty() short_name = StringProperty() + label = StringProperty() arm_code = StringProperty() description = StringProperty() randomization_group = StringProperty() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_repository.py index cb27395a..1e639e75 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_repository.py @@ -1,4 +1,4 @@ -from neomodel import NodeSet +from neomodel import NodeSet, db from neomodel.sync_.match import ( Collect, NodeNameResolver, @@ -157,8 +157,21 @@ def _get_or_create_instance( state=ar.sponsor_model_dataset_vo.state, extended_domain=ar.sponsor_model_dataset_vo.extended_domain, ) + self._db_save_node(new_instance) + # Add extra properties using Cypher (neomodel only saves defined properties) + if ar.sponsor_model_dataset_vo.extra_properties: + # Sanitize key names for Neo4j (replace spaces and dashes with underscores) + sanitized_props = { + key.replace(" ", "_").replace("-", "_"): value + for key, value in ar.sponsor_model_dataset_vo.extra_properties.items() + } + db.cypher_query( + "MATCH (n) WHERE elementId(n) = $element_id SET n += $extra_props", + {"element_id": new_instance.element_id, "extra_props": sanitized_props}, + ) + # Connect with root root.has_sponsor_model_instance.connect(new_instance) @@ -222,6 +235,41 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( sponsor_model_version, enrich_build_order, ) = get_sponsor_model_info_from_dataset(value) + + # Extract extra properties from the Neo4j node + known_fields = { + "is_basic_std", + "xml_path", + "xml_title", + "structure", + "purpose", + "is_cdisc_std", + "source_ig", + "standard_ref", + "comment", + "ig_comment", + "map_domain_flag", + "suppl_qual_flag", + "include_in_raw", + "gen_raw_seqno_flag", + "label", + "state", + "extended_domain", + "id", + "element_id", + } + extra_props = {} + for key in dir(value): + if not key.startswith("_") and hasattr(value, key): + attr = getattr(value, key) + # Only include simple data types (not methods, relationships, etc.) + if ( + key not in known_fields + and not callable(attr) + and not hasattr(attr, "_all") + ): + extra_props[key] = attr + return SponsorModelDatasetAR.from_repository_values( dataset_uid=root.uid, sponsor_model_dataset_vo=SponsorModelDatasetVO.from_repository_values( @@ -249,6 +297,7 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( label=value.label, state=value.state, extended_domain=value.extended_domain, + extra_properties=extra_props if extra_props else None, ), library=LibraryVO.from_input_values_2( library_name=library.name, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_variable_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_variable_repository.py index 802c36e6..c83a408e 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_variable_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/sponsor_model_dataset_variable_repository.py @@ -180,8 +180,21 @@ def _get_or_create_instance( enrich_build_order=ar.sponsor_model_dataset_variable_vo.enrich_build_order, enrich_rule=ar.sponsor_model_dataset_variable_vo.enrich_rule, ) + self._db_save_node(new_instance) + # Add extra properties using Cypher (neomodel only saves defined properties) + if ar.sponsor_model_dataset_variable_vo.extra_properties: + # Sanitize key names for Neo4j (replace spaces and dashes with underscores) + sanitized_props = { + key.replace(" ", "_").replace("-", "_"): value + for key, value in ar.sponsor_model_dataset_variable_vo.extra_properties.items() + } + db.cypher_query( + "MATCH (n) WHERE elementId(n) = $element_id SET n += $extra_props", + {"element_id": new_instance.element_id, "extra_props": sanitized_props}, + ) + # Connect with root root.has_sponsor_model_instance.connect(new_instance) @@ -283,6 +296,60 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( sponsor_model_version, _, ) = get_sponsor_model_info_from_dataset(dataset_value, return_ordinal=False) + + # Extract extra properties from the Neo4j node + known_fields = { + "is_basic_std", + "label", + "variable_type", + "length", + "display_format", + "xml_datatype", + "core", + "origin", + "origin_type", + "origin_source", + "role", + "term", + "algorithm", + "qualifiers", + "is_cdisc_std", + "comment", + "ig_comment", + "class_table", + "class_column", + "map_var_flag", + "fixed_mapping", + "include_in_raw", + "nn_internal", + "value_lvl_where_cols", + "value_lvl_label_col", + "value_lvl_collect_ct_val", + "value_lvl_ct_codelist_id_col", + "enrich_build_order", + "enrich_rule", + "id", + "element_id", + "implemented_variable_class_inconsistency", + "implemented_variable_class_uid", + "implemented_parent_dataset_class_uid", + "implemented_parent_dataset_class", + "implemented_variable_class", + "references_codelist", + "references_terms", + } + extra_props = {} + for key in dir(value): + if not key.startswith("_") and hasattr(value, key): + attr = getattr(value, key) + # Only include simple data types (not methods, relationships, etc.) + if ( + key not in known_fields + and not callable(attr) + and not hasattr(attr, "_all") + ): + extra_props[key] = attr + return SponsorModelDatasetVariableAR.from_repository_values( variable_uid=root.uid, sponsor_model_dataset_variable_vo=SponsorModelDatasetVariableVO.from_repository_values( @@ -324,6 +391,7 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( value_lvl_ct_codelist_id_col=value.value_lvl_ct_codelist_id_col, enrich_build_order=value.enrich_build_order, enrich_rule=value.enrich_rule, + extra_properties=extra_props if extra_props else None, ), library=LibraryVO.from_input_values_2( library_name=library.name, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/standard_data_model_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/standard_data_model_repository.py index 071af389..456befd9 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/standard_data_model_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/standard_data_model_repository.py @@ -14,6 +14,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, validate_filters_and_add_search_string, ) from common.exceptions import NotFoundException, ValidationException @@ -210,16 +211,20 @@ def find_all( result_array, attributes_names ) - count_result, _ = db.cypher_query( - query=query.count_query, params=query.parameters + total_amount = calculate_total_count_from_query_result( + len(extracted_items), page_number, page_size, total_count ) - if len(count_result) > 0 and total_count: - if query.union_match_clause: - total_amount = count_result[0][0] + count_result[1][0] + if total_amount is None: + count_result, _ = db.cypher_query( + query=query.count_query, params=query.parameters + ) + if len(count_result) > 0 and total_count: + if query.union_match_clause: + total_amount = count_result[0][0] + count_result[1][0] + else: + total_amount = count_result[0][0] else: - total_amount = count_result[0][0] - else: - total_amount = 0 + total_amount = 0 return extracted_items, total_amount 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 e300a1b3..e9e0437b 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 @@ -377,6 +377,7 @@ def copy_study_items( MATCH (selection_src:StudySelection&(" + to_copy_labels_text + "))<-[sv_has_selection]- (sv_src) + WHERE type(sv_has_selection) <> 'HAS_PROTOCOL_SOA_CELL' AND type(sv_has_selection) <> 'HAS_PROTOCOL_SOA_FOOTNOTE' MATCH (sr_src)-[audit_trail:AUDIT_TRAIL]-> (saction_src:StudyAction)-[saction_selection:AFTER]-> @@ -420,12 +421,13 @@ def copy_study_items( query = """ WITH $study_src_uid as study_src, $study_target_uid as study_target, $to_copy_labels as to_copy_labels -MATCH (sr_from:StudyRoot)-[:LATEST]->(sv_from:StudyValue)-->(selection_src_from:StudySelection)-[r_ext_src]->(selection_src_to:StudySelection) - where sr_from.uid = study_src +MATCH (sr_from:StudyRoot)-[:LATEST]->(sv_from:StudyValue)-[sv_to_ss_source]->(selection_src_from:StudySelection)-[r_ext_src]->(selection_src_to:StudySelection) + where sr_from.uid = study_src AND type(sv_to_ss_source) <> "HAS_PROTOCOL_SOA_CELL" AND type(sv_to_ss_source) <> "HAS_PROTOCOL_SOA_FOOTNOTE" WITH selection_src_from.uid as from_uid, type(r_ext_src) AS from_rel_type_to, selection_src_to.uid as to_uid, study_target, to_copy_labels match (sr_to:StudyRoot)-[:LATEST]->(sv_to:StudyValue)-->(a:StudySelection) where sr_to.uid = study_target -match (sr_to)-[:LATEST]->(sv_to)-->(b:StudySelection) +match (sr_to)-[:LATEST]->(sv_to)-[sv_to__b]->(b:StudySelection) + WHERE type(sv_to__b) <> "HAS_PROTOCOL_SOA_CELL" AND type(sv_to__b) <> "HAS_PROTOCOL_SOA_FOOTNOTE" WITH a,b, from_rel_type_to where a.uid = from_uid and b.uid = to_uid 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 085eb666..911ae680 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 @@ -78,6 +78,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, ) from clinical_mdr_api.services._utils import calculate_diffs from clinical_mdr_api.services.user_info import UserInfoService @@ -2533,7 +2534,10 @@ def _retrieve_all_snapshots( study_dictionary[attribute_name] = study_property studies.append(study_dictionary) - if total_count: + total = calculate_total_count_from_query_result( + len(studies), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) @@ -2541,8 +2545,6 @@ def _retrieve_all_snapshots( total = count_result[0][0] else: total = 0 - else: - total = 0 return GenericFilteringReturn( items=self._retrieve_all_snapshots_from_cypher_query_result( @@ -2684,7 +2686,10 @@ def _retrieve_study_snapshot_history( for study_property, attribute_name in zip(study, attributes_names): study_dictionary[attribute_name] = study_property studies.append(study_dictionary) - if total_count: + total = calculate_total_count_from_query_result( + len(studies), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) @@ -2692,8 +2697,6 @@ def _retrieve_study_snapshot_history( total = count_result[0][0] else: total = 0 - else: - total = 0 return GenericFilteringReturn( items=self._retrieve_all_snapshots_from_cypher_query_result(studies), diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/base.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/base.py index 8ab59af5..f73117db 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/base.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/base.py @@ -1,6 +1,9 @@ import datetime from dataclasses import dataclass +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import ( Create, @@ -34,6 +37,10 @@ def _from_repository_values(self, study_uid: str, selection): """Must be defined by subclasses.""" raise NotImplementedError + def exclude_relationships(self): + """Must be defined by subclasses.""" + raise NotImplementedError + def perform_save( self, study_value_node: StudyValue, @@ -51,27 +58,32 @@ def save(self, selection_vo, author_id: str): ) latest_study_value_node = study_root_node.latest_value.single() - selection = self.perform_save(latest_study_value_node, selection_vo, author_id) + new_selection = self.perform_save( + latest_study_value_node, selection_vo, author_id + ) # Update audit trail - before_audit_node = None if selection_vo.uid is not None: - before_audit_node = Edit( - author_id=author_id, date=datetime.datetime.now(datetime.timezone.utc) + selection = self.get_study_selection( + latest_study_value_node, selection_vo.uid + ) + _manage_versioning_with_relations( + study_root=study_root_node, + action_type=Edit, + before=selection, + after=new_selection, + exclude_relationships=self.exclude_relationships(), + author_id=author_id, ) - before_audit_node.save() - study_root_node.audit_trail.connect(before_audit_node) - before_audit_node.has_before.connect(selection) - after_audit_node = Edit() else: - after_audit_node = Create() - - after_audit_node.author_id = author_id - after_audit_node.date = datetime.datetime.now(datetime.timezone.utc) - after_audit_node.save() - study_root_node.audit_trail.connect(after_audit_node) - after_audit_node.has_after.connect(selection) + _manage_versioning_with_relations( + study_root=study_root_node, + action_type=Create, + before=None, + after=new_selection, + author_id=author_id, + ) - return self._from_repository_values(selection_vo.study_uid, selection) + return self._from_repository_values(selection_vo.study_uid, new_selection) def get_study_selection(self, study_value_node: StudyValue, selection_uid: str): """Must be defined by subclasses.""" @@ -90,13 +102,14 @@ def delete(self, study_uid: str, selection_uid: str, author_id: str) -> None: latest_study_value_node, selection_vo, author_id ) # Audit trail - audit_node = Delete( - author_id=author_id, date=datetime.datetime.now(datetime.timezone.utc) + _manage_versioning_with_relations( + study_root=study_root_node, + action_type=Delete, + before=selection, + after=new_selection, + exclude_relationships=self.exclude_relationships(), + author_id=author_id, ) - audit_node.save() - study_root_node.audit_trail.connect(audit_node) - audit_node.has_before.connect(selection) - audit_node.has_after.connect(new_selection) new_selection.study_value.disconnect(latest_study_value_node) # Delete relation selection.study_value.disconnect(latest_study_value_node) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_base_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_base_repository.py index 0efdbe19..7f719235 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_base_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_base_repository.py @@ -77,6 +77,7 @@ def get_study_selection_node_from_latest_study_value( @abc.abstractmethod def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionBaseVO, @@ -248,9 +249,7 @@ def is_repository_based_on_ordered_selection(self): def _get_audit_trail_nodes_to_reference( self, - study_root_node: StudyRoot, latest_study_value_node: StudyValue, - author_id: str, study_activity: StudySelectionBaseVO, study_selection: StudySelectionBaseAR, ): @@ -263,11 +262,10 @@ def _get_audit_trail_nodes_to_reference( audit_node = self._get_audit_node( study_selection, study_activity.study_selection_uid ) - audit_node = self._set_before_audit_info( - last_study_selection_node, audit_node, study_root_node, author_id - ) + return audit_node, last_study_selection_node + # pylint: disable=unused-argument @trace_calls def save( self, @@ -342,9 +340,7 @@ def save( ): audit_node, last_study_selection_node = ( self._get_audit_trail_nodes_to_reference( - study_root_node=study_root_node, latest_study_value_node=latest_study_value_node, - author_id=author_id, study_activity=study_activity, study_selection=study_selection, ) @@ -356,6 +352,7 @@ def save( if isinstance(audit_node, Delete): self._add_new_selection( + study_root_node, latest_study_value_node, order, study_activity, @@ -378,21 +375,16 @@ def save( ): audit_node, last_study_selection_node = ( self._get_audit_trail_nodes_to_reference( - study_root_node=study_root_node, latest_study_value_node=latest_study_value_node, - author_id=author_id, study_activity=selection, study_selection=study_selection, ) ) else: audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( + study_root_node, latest_study_value_node, order, selection, @@ -401,21 +393,6 @@ def save( False, ) - @staticmethod - def _set_before_audit_info( - study_activity_selection_node: StudySelection, - audit_node: StudyAction, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_activity_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @trace_calls def _get_selection_with_history( self, study_uid: str, study_selection_uid: str | None = None 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 5b7985f2..b137dc42 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 @@ -5,13 +5,13 @@ from neomodel import db 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.activities import ( ActivityGroupRoot, ActivityGroupValue, ) -from clinical_mdr_api.domain_repositories.models.study import StudyValue +from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import StudyAction from clinical_mdr_api.domain_repositories.models.study_selections import ( StudyActivityGroup, @@ -229,6 +229,7 @@ def get_study_selection_node_from_latest_study_value( def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionActivityGroupVO, @@ -262,18 +263,18 @@ def _add_new_selection( study_activity_group_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_activity_group_selection_node) # Connect new node with Activity subgroup value study_activity_group_selection_node.has_selected_activity_group.connect( latest_activity_group_value_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_group_selection_node, - ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=last_study_selection_node, + after=study_activity_group_selection_node, + exclude_relationships=[ActivityGroupValue], + author_id=selection.author_id, + ) def generate_uid(self) -> str: return StudyActivityGroup.get_next_free_uid_and_increment_counter() 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 d50256ec..bff2d6b0 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,22 +2,21 @@ from dataclasses import dataclass from typing import Any +from neomodel import db + 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, + _manage_versioning_with_relations, ) from clinical_mdr_api.domain_repositories.models._utils import ListDistinct -from clinical_mdr_api.domain_repositories.models.activities import ( - ActivityInstanceRoot, - ActivityInstanceValue, -) +from clinical_mdr_api.domain_repositories.models.activities import 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 import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import StudyAction from clinical_mdr_api.domain_repositories.models.study_selections import ( StudyActivity, @@ -38,8 +37,13 @@ StudySelectionActivityInstanceVO, ) from clinical_mdr_api.models.study_selections.study_visit import SimpleStudyVisit +from common import exceptions from common.config import settings -from common.exceptions import BusinessLogicException, NotFoundException +from common.exceptions import ( + BusinessLogicException, + NotFoundException, + ValidationException, +) from common.utils import convert_to_datetime @@ -712,6 +716,7 @@ def get_study_selection_node_from_latest_study_value( def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionActivityInstanceVO, @@ -719,6 +724,67 @@ def _add_new_selection( last_study_selection_node: StudyActivityInstance, for_deletion: bool = False, ): + # Fetch nodes referenced by uids + query = [ + "MATCH (study_activity:StudyActivity {uid:$study_activity_uid}) WHERE NOT (study_activity)-[:BEFORE]-()", + ] + params = { + "study_activity_uid": selection.study_activity_uid, + } + returns = ["study_activity"] + if selection.activity_instance_uid: + if selection.activity_instance_version: + query.append( + """MATCH (instance_root:ActivityInstanceRoot {uid: $activity_instance_uid}) + -[:HAS_VERSION {version: $activity_instance_version}]->(latest_activity_instance_value:ActivityInstanceValue) WITH * LIMIT 1""" + ) + params["activity_instance_version"] = ( + selection.activity_instance_version + ) + else: + query.append( + "MATCH (instance_root:ActivityInstanceRoot {uid: $activity_instance_uid})-[:LATEST]->(latest_activity_instance_value:ActivityInstanceValue)" + ) + params["activity_instance_uid"] = selection.activity_instance_uid + returns.append("latest_activity_instance_value") + if selection.study_data_supplier_uid: + query.append( + "MATCH (study_data_supplier:StudyDataSupplier {uid: $study_data_supplier_uid}) WHERE NOT (study_data_supplier)-[:BEFORE]-()" + ) + params["study_data_supplier_uid"] = selection.study_data_supplier_uid + returns.append("study_data_supplier") + if selection.origin_type_uid: + query.append( + "OPTIONAL MATCH (origin_type_root:CTTermRoot {uid: $origin_type_uid})" + ) + params["origin_type_uid"] = selection.origin_type_uid + returns.append("origin_type_root") + if selection.origin_source_uid: + query.append( + "OPTIONAL MATCH (origin_source_root:CTTermRoot {uid: $origin_source_uid})" + ) + params["origin_source_uid"] = selection.origin_source_uid + returns.append("origin_source_root") + + 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.BusinessLogicException( + msg=f"There should be one row returned with dependencies for StudyActivityInstance '{selection.study_selection_uid}'." + ) + + nodes = dict(zip(keys, results[0])) + latest_activity_instance_value_node: ActivityInstanceValue | None = nodes.get( + "latest_activity_instance_value" + ) + study_activity_node: StudyActivity = nodes["study_activity"] + study_data_supplier_node: StudyDataSupplier | None = nodes.get( + "study_data_supplier" + ) + origin_type_root: CTTermRoot | None = nodes.get("origin_type_root") + origin_source_root: CTTermRoot | None = nodes.get("origin_source_root") + # 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 @@ -756,40 +822,20 @@ def _add_new_selection( is_reviewed=selection.is_reviewed, is_important=selection.is_important, accepted_version=selection.accepted_version, - ) - study_activity_instance_selection_node.save() + ).save() if not for_deletion: # Connect new node with study value latest_study_value_node.has_study_activity_instance.connect( study_activity_instance_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_activity_instance_selection_node) - if selection.activity_instance_uid: - # find the activity instance value - activity_instance_root_node: ActivityInstanceRoot = ( - ActivityInstanceRoot.nodes.get(uid=selection.activity_instance_uid) - ) - latest_activity_instance_value_node: ActivityInstanceValue - if selection.activity_instance_version: - latest_activity_instance_value_node = ( - activity_instance_root_node.get_value_for_version( - selection.activity_instance_version - ) - ) - else: - latest_activity_instance_value_node = ( - activity_instance_root_node.has_latest_value.get() - ) + + if selection.activity_instance_uid and latest_activity_instance_value_node: # Connect new node with Activity value study_activity_instance_selection_node.has_selected_activity_instance.connect( latest_activity_instance_value_node ) # Connect StudyActivityInstance with StudyActivity node - study_activity_node = StudyActivity.nodes.has(has_before=False).get( - uid=selection.study_activity_uid - ) study_activity_instance_selection_node.study_activity_has_study_activity_instance.connect( study_activity_node ) @@ -827,60 +873,57 @@ def _add_new_selection( ) # 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 - ) + if selection.study_data_supplier_uid and 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 + ValidationException.raise_if( + origin_type_root is None, + msg=f"Origin Type Term with UID '{selection.origin_type_uid}' doesn't exist.", ) - 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 + 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 + ValidationException.raise_if( + origin_source_root is None, + msg=f"Origin Source Term with UID '{selection.origin_source_uid}' doesn't exist.", ) - 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 + selected_term_node = ( + CTCodelistAttributesRepository().get_or_create_selected_term( + origin_source_root, + codelist_submission_value=settings.origin_source_cl_submval, ) - - 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, - # StudyDataSupplier and CTTermContext are excluded because they're handled explicitly above - exclude_study_selection_relationships=[ - StudyActivity, - StudyVisit, - StudyDataSupplier, - CTTermContext, - ], ) + study_activity_instance_selection_node.has_origin_source.connect( + selected_term_node + ) + + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=last_study_selection_node, + after=study_activity_instance_selection_node, + exclude_relationships=[ + ActivityInstanceValue, + StudyActivity, + StudyVisit, + StudyDataSupplier, + CTTermContext, + ], + author_id=selection.author_id, + ) def generate_uid(self) -> str: return StudyActivityInstance.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instruction_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instruction_repository.py index 6def5c0a..f6a8604a 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instruction_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instruction_repository.py @@ -1,10 +1,13 @@ -from neomodel import db +from neomodel import RelationshipDefinition, StructuredNode, db from clinical_mdr_api.domain_repositories.models.study import StudyValue from clinical_mdr_api.domain_repositories.models.study_selections import ( StudyActivityInstruction, ) -from clinical_mdr_api.domain_repositories.models.syntax import ActivityInstructionRoot +from clinical_mdr_api.domain_repositories.models.syntax import ( + ActivityInstructionRoot, + ActivityInstructionValue, +) from clinical_mdr_api.domain_repositories.study_selections import base from clinical_mdr_api.domains.study_selections.study_activity_instruction import ( StudyActivityInstructionVO, @@ -29,6 +32,11 @@ def _from_repository_values( author_id=study_action.author_id, ) + def exclude_relationships( + self, + ) -> list[type[StructuredNode] | RelationshipDefinition | str]: + return [ActivityInstructionValue] + def perform_save( self, study_value_node: StudyValue, @@ -64,8 +72,7 @@ def perform_save( self._remove_old_selection_if_exists(selection_vo.study_uid, selection_vo) # Create new node - node = StudyActivityInstruction(uid=selection_vo.uid) - node.save() + node = StudyActivityInstruction(uid=selection_vo.uid).save() # Create relations node.study_activity.connect(study_activity_node) 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 7790580f..d1a55a70 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 @@ -6,13 +6,10 @@ 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.activities import ( - ActivityRoot, - ActivityValue, -) -from clinical_mdr_api.domain_repositories.models.study import StudyValue +from clinical_mdr_api.domain_repositories.models.activities import ActivityValue +from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import StudyAction from clinical_mdr_api.domain_repositories.models.study_selections import ( StudyActivity, @@ -28,6 +25,7 @@ StudySelectionActivityAR, StudySelectionActivityVO, ) +from common import exceptions from common.telemetry import trace_calls from common.utils import convert_to_datetime @@ -376,6 +374,7 @@ def get_study_selection_node_from_latest_study_value( def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionActivityVO, @@ -383,71 +382,101 @@ def _add_new_selection( last_study_selection_node: StudyActivity, for_deletion: bool = False, ): - # find the activity value - activity_root_node: ActivityRoot = ActivityRoot.nodes.get( - uid=selection.activity_uid + # Fetch nodes referenced by uids + query = [ + "MATCH (activity_root:ActivityRoot {uid: $activity_uid})-[:HAS_VERSION {version: $activity_version}]->(latest_activity_value:ActivityValue) WITH * LIMIT 1", + "MATCH (study_soa_group:StudySoAGroup {uid:$study_soa_group_uid}) WHERE NOT (study_soa_group)-[:BEFORE]-()", + ] + params = { + "study_uid": selection.study_uid, + "activity_uid": selection.activity_uid, + "activity_version": selection.activity_version, + "study_soa_group_uid": selection.study_soa_group_uid, + } + returns = ["latest_activity_value", "study_soa_group"] + if selection.study_activity_subgroup_uid: + query.append( + "MATCH (study_activity_subgroup:StudyActivitySubGroup {uid: $study_activity_subgroup_uid}) WHERE NOT (study_activity_subgroup)-[:BEFORE]-()" + ) + params["study_activity_subgroup_uid"] = ( + selection.study_activity_subgroup_uid + ) + returns.append("study_activity_subgroup") + if selection.study_activity_group_uid: + query.append( + "MATCH (study_activity_group:StudyActivityGroup {uid: $study_activity_group_uid}) WHERE NOT (study_activity_group)-[:BEFORE]-()" + ) + params["study_activity_group_uid"] = selection.study_activity_group_uid + returns.append("study_activity_group") + + 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.BusinessLogicException( + msg=f"There should be one row returned with dependencies for StudyActivity '{selection.study_selection_uid}'." + ) + + nodes = dict(zip(keys, results[0])) + latest_activity_value_node: ActivityValue = nodes["latest_activity_value"] + study_soa_group_node: StudySoAGroup = nodes["study_soa_group"] + study_activity_subgroup_node: StudyActivitySubGroup | None = nodes.get( + "study_activity_subgroup" ) - latest_activity_value_node: ActivityValue = ( - activity_root_node.get_value_for_version(selection.activity_version) + study_activity_group_node: StudyActivityGroup | None = nodes.get( + "study_activity_group" ) + # Create new activity selection study_activity_selection_node = StudyActivity( + uid=selection.study_selection_uid, order=order, show_activity_in_protocol_flowchart=selection.show_activity_in_protocol_flowchart, keep_old_version=selection.keep_old_version, keep_old_version_date=selection.keep_old_version_date, - ) - study_activity_selection_node.uid = selection.study_selection_uid - study_activity_selection_node.accepted_version = selection.accepted_version - study_activity_selection_node.save() + accepted_version=selection.accepted_version, + ).save() if not for_deletion: # Connect new node with study value latest_study_value_node.has_study_activity.connect( study_activity_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_activity_selection_node) + # Connect new node with Activity value study_activity_selection_node.has_selected_activity.connect( latest_activity_value_node ) # Connect StudyActivity with StudySoAGroup node - study_soa_group_node = StudySoAGroup.nodes.has(has_before=False).get( - uid=selection.study_soa_group_uid - ) study_activity_selection_node.has_soa_group_selection.connect( study_soa_group_node ) if selection.study_activity_subgroup_uid: # Connect StudyActivity with StudyActivitySubGroup node - study_activity_subgroup = StudyActivitySubGroup.nodes.has( - has_before=False - ).get(uid=selection.study_activity_subgroup_uid) study_activity_selection_node.study_activity_has_study_activity_subgroup.connect( - study_activity_subgroup + study_activity_subgroup_node ) if selection.study_activity_group_uid: # Connect StudyActivity with StudyActivityGroup node - study_activity_group = StudyActivityGroup.nodes.has(has_before=False).get( - uid=selection.study_activity_group_uid - ) study_activity_selection_node.study_activity_has_study_activity_group.connect( - study_activity_group - ) - 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_selection_node, - exclude_study_selection_relationships=[ - StudySoAGroup, - StudyActivitySubGroup, - StudyActivityGroup, - ], + study_activity_group_node ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=last_study_selection_node, + after=study_activity_selection_node, + exclude_relationships=[ + ActivityValue, + StudySoAGroup, + StudyActivitySubGroup, + StudyActivityGroup, + ], + author_id=selection.author_id, + ) + def generate_uid(self) -> str: return StudyActivity.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_schedule_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_schedule_repository.py index ff611694..e9af711e 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_schedule_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_schedule_repository.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import Any -from neomodel import db +from neomodel import RelationshipDefinition, StructuredNode, db from clinical_mdr_api import utils from clinical_mdr_api.domain_repositories.models._utils import ListDistinct @@ -10,6 +10,7 @@ StudyActivity, StudyActivitySchedule, ) +from clinical_mdr_api.domain_repositories.models.study_visit import StudyVisit from clinical_mdr_api.domain_repositories.study_selections import base from clinical_mdr_api.domains.study_selections.study_activity_schedule import ( StudyActivityScheduleVO, @@ -53,6 +54,11 @@ def _from_repository_values( author_id=study_action.author_id, ) + def exclude_relationships( + self, + ) -> list[type[StructuredNode] | RelationshipDefinition | str]: + return [StudyActivity, StudyVisit] + def perform_save( self, study_value_node: StudyValue, @@ -74,8 +80,7 @@ def perform_save( ) # Create new node - schedule = StudyActivitySchedule(uid=selection_vo.uid) - schedule.save() + schedule = StudyActivitySchedule(uid=selection_vo.uid).save() study_activity_node = study_value_node.has_study_activity.get_or_none( uid=selection_vo.study_activity_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 75ef112b..94c21ca7 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 @@ -5,13 +5,13 @@ from neomodel import db 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.activities import ( ActivitySubGroupRoot, ActivitySubGroupValue, ) -from clinical_mdr_api.domain_repositories.models.study import StudyValue +from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import StudyAction from clinical_mdr_api.domain_repositories.models.study_selections import ( StudyActivitySubGroup, @@ -223,6 +223,7 @@ def get_study_selection_node_from_latest_study_value( def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionActivitySubGroupVO, @@ -256,18 +257,18 @@ def _add_new_selection( study_activity_subgroup_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_activity_subgroup_selection_node) # Connect new node with Activity subgroup value study_activity_subgroup_selection_node.has_selected_activity_subgroup.connect( latest_activity_subgroup_value_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_subgroup_selection_node, - ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=last_study_selection_node, + after=study_activity_subgroup_selection_node, + exclude_relationships=[ActivitySubGroupValue], + author_id=selection.author_id, + ) def generate_uid(self) -> str: return StudyActivitySubGroup.get_next_free_uid_and_increment_counter() 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 18ae30c8..0c38e171 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 @@ -12,9 +12,10 @@ CTCodelistAttributesRepository, ) 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.controlled_terminology import ( + CTTermContext, CTTermRoot, ) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue @@ -44,6 +45,7 @@ class SelectionHistoryArm: study_uid: str | None arm_name: str arm_short_name: str + arm_label: str | None arm_code: str | None arm_description: str | None arm_randomization_group: str | None @@ -161,6 +163,7 @@ def get_arms_branches_and_cohorts( sar.uid AS uid, sar.name AS name, sar.short_name AS short_name, + sar.label AS label, apoc.coll.sum([cohort in study_cohorts WHERE cohort.number_of_subjects is not null | cohort.number_of_subjects])AS number_of_subjects, study_cohorts """ @@ -232,6 +235,7 @@ def _retrieves_all_data( sar.uid AS study_selection_uid, sar.name AS arm_name, sar.short_name AS arm_short_name, + sar.label AS arm_label, sar.arm_code AS arm_code, sar.description AS arm_description, sar.order AS order, @@ -261,6 +265,7 @@ def _retrieves_all_data( study_uid=selection["study_uid"], name=selection["arm_name"], short_name=selection["arm_short_name"], + label=selection["arm_label"], code=selection["arm_code"], description=selection["arm_description"], study_selection_uid=selection["study_selection_uid"], @@ -350,6 +355,7 @@ def _get_audit_node( return Create() return Delete() + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionArmAR, author_id: str) -> None: """ Persist the set of selected study arms from the aggregate to the database @@ -408,19 +414,14 @@ def save(self, study_selection: StudySelectionArmAR, author_id: str) -> None: audit_node = self._get_audit_node( study_selection, selection.study_selection_uid ) - # create the before node to the last_study_selection_node and audit trial to study_root - audit_node = self._set_before_audit_info( - audit_node=audit_node, - study_selection_node=last_study_selection_node, - study_root_node=study_root_node, - author_id=author_id, - ) + # storage of the removed node audit trail to after put the "after" relationship to the new one audit_trail_nodes[selection.study_selection_uid] = audit_node # storage of the removed node to after get its connections last_nodes[selection.study_selection_uid] = last_study_selection_node if isinstance(audit_node, Delete): self._add_new_selection( + study_root_node, latest_study_value_node, order, selection, @@ -442,11 +443,9 @@ def save(self, study_selection: StudySelectionArmAR, author_id: str) -> None: else: # if the audi_node doesn't exists, then create a new one audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) + self._add_new_selection( + study_root_node, latest_study_value_node, order, selection, @@ -455,21 +454,6 @@ def save(self, study_selection: StudySelectionArmAR, author_id: str) -> None: before_node=last_study_selection_node, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_selection_node: StudyArm, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @staticmethod def arm_exists_by( db_property: str, value: str, arm_vo: StudySelectionArmVO @@ -487,6 +471,7 @@ def arm_exists_by( @staticmethod def _add_new_selection( + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionArmVO, @@ -500,6 +485,7 @@ def _add_new_selection( order=order, name=selection.name, short_name=selection.short_name, + label=selection.label, arm_code=selection.code, description=selection.description, randomization_group=selection.randomization_group, @@ -512,16 +498,6 @@ def _add_new_selection( if not for_deletion: # Connect new node with study value latest_study_value_node.has_study_arm.connect(study_arm_selection_node) - # Connect new node with audit trail - audit_node.has_after.connect(study_arm_selection_node) - - if before_node is not None: - manage_previous_connected_study_selection_relationships( - previous_item=before_node, - study_value_node=latest_study_value_node, - new_item=study_arm_selection_node, - exclude_study_selection_relationships=[], - ) # check if arm type is set if selection.arm_type_uid: @@ -535,11 +511,19 @@ def _add_new_selection( catalogue_name=settings.sdtm_ct_catalogue_name, ) ) - # connect to node # pylint: disable=no-member study_arm_selection_node.arm_type.connect(selected_arm_type_node) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=study_arm_selection_node, + exclude_relationships=[CTTermContext], + author_id=selection.author_id, + ) + def generate_uid(self) -> str: return StudyArm.get_next_free_uid_and_increment_counter() @@ -598,6 +582,7 @@ def _get_selection_with_history( all_sa.uid AS study_selection_uid, all_sa.name AS arm_name, all_sa.short_name AS arm_short_name, + all_sa.label AS arm_label, all_sa.arm_code AS arm_code, all_sa.description AS arm_description, all_sa.order AS order, @@ -632,6 +617,7 @@ def _get_selection_with_history( study_uid=study_uid, arm_name=res["arm_name"], arm_short_name=res["arm_short_name"], + arm_label=res["arm_label"], arm_code=res["arm_code"], arm_description=res["arm_description"], arm_randomization_group=res["randomization_group"], diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_branch_arm_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_branch_arm_repository.py index ef3a80ef..ea408939 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_branch_arm_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_branch_arm_repository.py @@ -9,7 +9,7 @@ acquire_write_lock_study_value, ) 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.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import ( @@ -466,6 +466,7 @@ def branch_arm_specific_is_last_on_arm_root( ) return len(sdc_node) == 0 + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionBranchArmAR, author_id: str) -> None: """ Persist the set of selected study branch arms from the aggregate to the database @@ -567,13 +568,6 @@ def save(self, study_selection: StudySelectionBranchArmAR, author_id: str) -> No audit_node = self._get_audit_node( study_selection, selected_object.study_selection_uid ) - # create the before node to the last_study_selection_node and audit trial to study_root - audit_node = self._set_before_audit_info( - audit_node=audit_node, - study_selection_node=last_study_selection_node, - study_root_node=study_root_node, - author_id=author_id, - ) audit_trail_nodes[selected_object.study_selection_uid] = audit_node last_nodes[selected_object.study_selection_uid] = ( @@ -581,6 +575,7 @@ def save(self, study_selection: StudySelectionBranchArmAR, author_id: str) -> No ) if isinstance(audit_node, Delete): self._add_new_selection( + study_root_node, latest_study_value_node, order, selected_object, @@ -602,11 +597,8 @@ def save(self, study_selection: StudySelectionBranchArmAR, author_id: str) -> No ] else: audit_node = Create() - audit_node.author_id = selected_object.author_id - audit_node.date = selected_object.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( + study_root_node, latest_study_value_node, order, selected_object, @@ -615,21 +607,6 @@ def save(self, study_selection: StudySelectionBranchArmAR, author_id: str) -> No before_node=last_study_selection_node, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_selection_node: StudyBranchArm, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @staticmethod def branch_arm_arm_update_conflict( branch_arm_vo: StudySelectionBranchArmVO, @@ -669,6 +646,7 @@ def branch_arm_exists_by( @staticmethod def _add_new_selection( + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionBranchArmVO, @@ -702,16 +680,6 @@ def _add_new_selection( latest_study_value_node.has_study_branch_arm.connect( study_branch_arm_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_branch_arm_selection_node) - - if before_node is not None: - manage_previous_connected_study_selection_relationships( - previous_item=before_node, - study_value_node=latest_study_value_node, - new_item=study_branch_arm_selection_node, - exclude_study_selection_relationships=[StudyArm], - ) # check if arm root is set if selection.arm_root_uid: @@ -731,6 +699,15 @@ def _add_new_selection( # connect to node study_branch_arm_selection_node.has_cohort.connect(study_cohort) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=study_branch_arm_selection_node, + exclude_relationships=[StudyArm, StudyCohort], + author_id=selection.author_id, + ) + def generate_uid(self) -> str: return StudyBranchArm.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_cohort_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_cohort_repository.py index c2d7ea22..f2d103ab 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_cohort_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_cohort_repository.py @@ -8,6 +8,9 @@ from clinical_mdr_api.domain_repositories._utils.helpers import ( acquire_write_lock_study_value, ) +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import ( Create, @@ -285,6 +288,7 @@ def cohort_exists_by( ) return cohort_node + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionCohortAR, author_id: str) -> None: """ Persist the set of selected study cohorts from the aggregate to the database @@ -351,13 +355,6 @@ def save(self, study_selection: StudySelectionCohortAR, author_id: str) -> None: audit_node = self._get_audit_node( study_selection, selection.study_selection_uid ) - # create the before node to the last_study_selection_node and audit trial to study_root - audit_node = self._set_before_audit_info( - audit_node=audit_node, - study_selection_node=last_study_selection_node, - study_root_node=study_root_node, - author_id=author_id, - ) self._remove_old_selection_if_exists(study_selection.study_uid, selection) # storage of the removed node audit trail to after put the "after" relationship to the new one audit_trail_nodes[selection.study_selection_uid] = audit_node @@ -365,31 +362,33 @@ def save(self, study_selection: StudySelectionCohortAR, author_id: str) -> None: last_nodes[selection.study_selection_uid] = last_study_selection_node if isinstance(audit_node, Delete): self._add_new_selection( + study_root=study_root_node, latest_study_value_node=latest_study_value_node, order=order, selection=selection, audit_node=audit_node, for_deletion=True, + before_node=last_study_selection_node, ) # loop through and add selections for order, selection in selections_to_add: + last_study_selection_node = None # if the study selection already has an audit trail node if selection.study_selection_uid in audit_trail_nodes: # extract the audit_trail_node audit_node = audit_trail_nodes[selection.study_selection_uid] + last_study_selection_node = last_nodes[selection.study_selection_uid] else: audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( + study_root=study_root_node, latest_study_value_node=latest_study_value_node, order=order, selection=selection, audit_node=audit_node, for_deletion=False, + before_node=last_study_selection_node, ) @staticmethod @@ -420,28 +419,15 @@ def _remove_old_selection_if_exists( }, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_selection_node: StudyCohort, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @staticmethod def _add_new_selection( + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionCohortVO, audit_node: StudyAction, for_deletion: bool = False, + before_node: StudyCohort | None = None, ): # Create new cohort selection study_cohort_selection_node = StudyCohort( @@ -461,8 +447,6 @@ def _add_new_selection( latest_study_value_node.has_study_cohort.connect( study_cohort_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_cohort_selection_node) # check if arm root is set if selection.arm_root_uids: @@ -488,6 +472,15 @@ def _add_new_selection( study_branch_arm_root ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=study_cohort_selection_node, + exclude_relationships=[StudyArm, StudyBranchArm], + author_id=selection.author_id, + ) + def generate_uid(self) -> str: return StudyCohort.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_dosing_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_dosing_repository.py index fc3ceafc..b0db8124 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_dosing_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_dosing_repository.py @@ -9,10 +9,15 @@ from clinical_mdr_api.domain_repositories.controlled_terminologies.ct_codelist_attributes_repository import ( CTCodelistAttributesRepository, ) +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models.concepts import ( NumericValueWithUnitRoot, + NumericValueWithUnitValue, ) from clinical_mdr_api.domain_repositories.models.controlled_terminology import ( + CTTermContext, CTTermRoot, ) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue @@ -103,28 +108,15 @@ def _remove_old_selection_if_exists( }, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_selection_node: StudyCompoundDosing, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @staticmethod def _add_new_selection( + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudyCompoundDosingVO, audit_node: StudyAction, for_deletion: bool = False, + before_node: StudyCompoundDosing | None = None, ): study_compound_node = latest_study_value_node.has_study_compound.get_or_none( uid=selection.study_compound_uid @@ -150,8 +142,6 @@ def _add_new_selection( if not for_deletion: # Connect new node with study value latest_study_value_node.has_study_compound_dosing.connect(selection_node) - # Connect new node with audit trail - audit_node.has_after.connect(selection_node) # Create relations selection_node.study_compound.connect(study_compound_node) @@ -188,6 +178,15 @@ def _add_new_selection( # connect to reason_for_missing node selection_node.has_dose_frequency.connect(selected_term_node) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=selection_node, + exclude_relationships=[NumericValueWithUnitValue, CTTermContext], + author_id=selection.author_id, + ) + def _get_audit_node( self, study_selection: StudySelectionCompoundDosingsAR, study_selection_uid: str ): @@ -205,6 +204,7 @@ def _get_audit_node( return Create() return Delete() + # pylint: disable=unused-argument def save( self, study_selection: StudySelectionCompoundDosingsAR, author_id: str ) -> None: @@ -253,6 +253,8 @@ def save( # audit trail nodes dictionary, holds the new nodes created for the audit trail audit_trail_nodes = {} + # dictionary of last nodes to traverse to their old connections + last_nodes = {} # loop through and remove selections for order, selection in selections_to_remove: @@ -265,30 +267,36 @@ def save( audit_node = self._get_audit_node( study_selection, selection.study_selection_uid ) - audit_node = self._set_before_audit_info( - audit_node, - last_study_selection_node, - study_root_node, - author_id, - ) audit_trail_nodes[selection.study_selection_uid] = audit_node + last_nodes[selection.study_selection_uid] = last_study_selection_node + if isinstance(audit_node, Delete): self._add_new_selection( - latest_study_value_node, order, selection, audit_node, True + study_root_node, + latest_study_value_node, + order, + selection, + audit_node, + True, + last_study_selection_node, ) # loop through and add selections for order, selection in selections_to_add: + last_study_selection_node = None if selection.study_selection_uid in audit_trail_nodes: audit_node = audit_trail_nodes[selection.study_selection_uid] + last_study_selection_node = last_nodes[selection.study_selection_uid] else: audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( - latest_study_value_node, order, selection, audit_node, False + study_root_node, + latest_study_value_node, + order, + selection, + audit_node, + False, + last_study_selection_node, ) def get_study_selection( diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_repository.py index 2cd4349b..c8d1b915 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_compound_repository.py @@ -17,12 +17,26 @@ from clinical_mdr_api.domain_repositories.controlled_terminologies.ct_codelist_attributes_repository import ( CTCodelistAttributesRepository, ) -from clinical_mdr_api.domain_repositories.models.compounds import CompoundAliasRoot +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) +from clinical_mdr_api.domain_repositories.models.compounds import ( + CompoundAliasRoot, + CompoundAliasValue, +) +from clinical_mdr_api.domain_repositories.models.concepts import ( + NumericValueWithUnitValue, +) from clinical_mdr_api.domain_repositories.models.controlled_terminology import ( + CTTermContext, CTTermRoot, ) from clinical_mdr_api.domain_repositories.models.medicinal_product import ( MedicinalProductRoot, + MedicinalProductValue, +) +from clinical_mdr_api.domain_repositories.models.pharmaceutical_product import ( + PharmaceuticalProductValue, ) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import ( @@ -419,6 +433,7 @@ def _get_audit_node( return Create() return Delete() + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionCompoundsAR, author_id: str) -> None: """ Persist the set of selected study compounds from the aggregate to the database @@ -465,6 +480,8 @@ def save(self, study_selection: StudySelectionCompoundsAR, author_id: str) -> No # audit trail nodes dictionary, holds the new nodes created for the audit trail audit_trail_nodes = {} + # dictionary of last nodes to traverse to their old connections + last_nodes = {} # loop through and remove selections for order, selection in selections_to_remove: @@ -475,30 +492,36 @@ def save(self, study_selection: StudySelectionCompoundsAR, author_id: str) -> No audit_node = self._get_audit_node( study_selection, selection.study_selection_uid ) - audit_node = self._set_before_audit_info( - audit_node, - last_study_selection_node, - study_root_node, - author_id, - ) audit_trail_nodes[selection.study_selection_uid] = audit_node + last_nodes[selection.study_selection_uid] = last_study_selection_node if isinstance(audit_node, Delete): self._add_new_selection( - latest_study_value_node, order, selection, audit_node, True + study_root_node, + latest_study_value_node, + order, + selection, + audit_node, + True, + last_study_selection_node, ) # loop through and add selections for order, selection in selections_to_add: + last_study_selection_node = None if selection.study_selection_uid in audit_trail_nodes: audit_node = audit_trail_nodes[selection.study_selection_uid] + last_study_selection_node = last_nodes[selection.study_selection_uid] else: audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) + self._add_new_selection( - latest_study_value_node, order, selection, audit_node, False + study_root_node, + latest_study_value_node, + order, + selection, + audit_node, + False, + last_study_selection_node, ) @staticmethod @@ -528,29 +551,15 @@ def _remove_old_selection_if_exists( }, ) - # TODO FIX StudyObjectiveSelection - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_objective_selection_node: StudyCompound, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_objective_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @staticmethod def _add_new_selection( + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionCompoundVO, audit_node: StudyAction, for_deletion: bool = False, + before_node: StudyCompound | None = None, ): # Create new compound selection study_compound_selection_node = StudyCompound(order=order).save() @@ -565,8 +574,6 @@ def _add_new_selection( latest_study_value_node.has_study_compound.connect( study_compound_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_compound_selection_node) # check if compound alias is set if selection.compound_alias_uid: @@ -677,6 +684,20 @@ def _add_new_selection( study_compound_selection_node.has_reason_for_missing.connect( selected_term_node ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=study_compound_selection_node, + exclude_relationships=[ + CompoundAliasValue, + MedicinalProductValue, + PharmaceuticalProductValue, + NumericValueWithUnitValue, + CTTermContext, + ], + author_id=selection.author_id, + ) def generate_uid(self) -> str: return StudyCompound.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_criteria_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_criteria_repository.py index b36fd7d5..61eae2ba 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_criteria_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_criteria_repository.py @@ -8,6 +8,9 @@ from clinical_mdr_api.domain_repositories._utils.helpers import ( acquire_write_lock_study_value, ) +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import ( Create, @@ -19,6 +22,8 @@ from clinical_mdr_api.domain_repositories.models.syntax import ( CriteriaRoot, CriteriaTemplateRoot, + CriteriaTemplateValue, + CriteriaValue, ) from clinical_mdr_api.domains.study_selections.study_selection_criteria import ( StudySelectionCriteriaAR, @@ -314,6 +319,7 @@ def _list_selections_to_add_or_remove( return selections_to_remove, selections_to_add + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionCriteriaAR, author_id: str) -> None: """ Persist the set of selected study criteria from the aggregate to the database @@ -349,6 +355,8 @@ def save(self, study_selection: StudySelectionCriteriaAR, author_id: str) -> Non # audit trail nodes dictionary, holds the new nodes created for the audit trail audit_trail_nodes = {} + # dictionary of last nodes to traverse to their old connections + last_nodes = {} # loop through and remove selections for criteria_list in selections_to_remove.values(): @@ -366,17 +374,19 @@ def save(self, study_selection: StudySelectionCriteriaAR, author_id: str) -> Non audit_node = self._get_audit_node( study_selection, selected_object.study_selection_uid ) - audit_node = self._set_before_audit_info( - audit_node, last_study_selection_node, study_root_node, author_id - ) audit_trail_nodes[selected_object.study_selection_uid] = audit_node + last_nodes[selected_object.study_selection_uid] = ( + last_study_selection_node + ) if isinstance(audit_node, Delete): self._add_new_selection( + study_root_node, latest_study_value_node, order, selected_object, audit_node, True, + last_study_selection_node, ) # loop through and add selections @@ -384,16 +394,22 @@ def save(self, study_selection: StudySelectionCriteriaAR, author_id: str) -> Non for selection in criteria_list: order = selection[0] selected_object = selection[1] + last_study_selection_node = None if selected_object.study_selection_uid in audit_trail_nodes: audit_node = audit_trail_nodes[selected_object.study_selection_uid] + last_study_selection_node = last_nodes[ + selected_object.study_selection_uid + ] else: audit_node = Create() - audit_node.author_id = selected_object.author_id - audit_node.date = selected_object.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( - latest_study_value_node, order, selected_object, audit_node, False + study_root_node, + latest_study_value_node, + order, + selected_object, + audit_node, + False, + last_study_selection_node, ) def _remove_old_selection_if_exists( @@ -410,28 +426,15 @@ def _remove_old_selection_if_exists( }, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_criteria_selection_node: StudyCriteria, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_criteria_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionCriteriaVO, audit_node: StudyAction, for_deletion: bool = False, + before_node: StudyCriteria | None = None, ): if selection.is_instance: # Get the criteria value @@ -464,8 +467,6 @@ def _add_new_selection( latest_study_value_node.has_study_criteria.connect( study_criteria_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_criteria_selection_node) # Connect new node with object value node if selection.is_instance: @@ -476,6 +477,17 @@ def _add_new_selection( study_criteria_selection_node.has_selected_criteria_template.connect( latest_criteria_template_value_node ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=study_criteria_selection_node, + exclude_relationships=[ + CriteriaValue, + CriteriaTemplateValue, + ], + author_id=selection.author_id, + ) def generate_uid(self) -> str: return StudyCriteria.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_class_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_class_repository.py index 2723a0fa..89e24be1 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_class_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_class_repository.py @@ -1,5 +1,6 @@ -import datetime - +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models._utils import ListDistinct from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import Create, Edit @@ -86,13 +87,13 @@ def post_study_design_class( )[0] latest_study_value.has_study_design_class.connect(study_design_class_node) - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), + _manage_versioning_with_relations( + study_root=study_root, + action_type=Create, + before=None, + after=study_design_class_node, author_id=author_id, ) - action.save() - action.has_after.connect(study_design_class_node) - study_root.audit_trail.connect(action) return self.get_study_design_class(study_uid=study_uid) @@ -116,13 +117,13 @@ def edit_study_design_class( )[0] latest_study_value.has_study_design_class.connect(study_design_class_node) - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), + _manage_versioning_with_relations( + study_root=study_root, + action_type=Edit, + before=previous_node, + after=study_design_class_node, + exclude_relationships=[], author_id=author_id, ) - action.save() - action.has_before.connect(previous_node) - action.has_after.connect(study_design_class_node) - study_root.audit_trail.connect(action) return self.get_study_design_class(study_uid=study_uid) 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 fc3fa4c6..f584dd03 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 @@ -1,4 +1,3 @@ -import datetime from typing import Any, TypeVar from neomodel import Q, db @@ -8,7 +7,7 @@ CTCodelistAttributesRepository, ) 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.controlled_terminology import ( @@ -78,13 +77,11 @@ def find_all_disease_milestone( filter_operator: FilterOperator = FilterOperator.AND, total_count: bool = False, study_value_version: str | None = None, - **kwargs, ) -> tuple[list[StudyDiseaseMilestoneOGM], int]: q_filters = self.create_query_filter_statement_neomodel( study_uid=study_uid, study_value_version=study_value_version, filter_by=filter_by, - **kwargs, ) q_filters = merge_q_query_filters(q_filters, filter_operator=filter_operator) sort_paths = get_order_by_clause( @@ -237,8 +234,6 @@ def _update( study_value is None, msg="Study doesn't have draft version." ) - if not create: - previous_item = study_value.has_study_disease_milestone.get(uid=item.uid) new_study_disease_milestone = StudyDiseaseMilestone( uid=item.uid, accepted_version=item.accepted_version, @@ -263,84 +258,40 @@ def _update( ) if create: - self.manage_versioning_create( - study_root=study_root, item=item, new_item=new_study_disease_milestone + _manage_versioning_with_relations( + study_root=study_root, + action_type=Create, + before=None, + after=new_study_disease_milestone, + author_id=self.author_id, ) new_study_disease_milestone.study_value.connect(study_value) else: + exclude_relationships = [StudyDiseaseMilestone.has_disease_milestone_type] + previous_item = study_value.has_study_disease_milestone.get(uid=item.uid) if delete is False: # update - self.manage_versioning_update( + _manage_versioning_with_relations( study_root=study_root, - item=item, - # pylint: disable=possibly-used-before-assignment - previous_item=previous_item, - new_item=new_study_disease_milestone, + action_type=Edit, + before=previous_item, + after=new_study_disease_milestone, + exclude_relationships=exclude_relationships, + author_id=self.author_id, ) new_study_disease_milestone.study_value.connect(study_value) else: # delete - self.manage_versioning_delete( + _manage_versioning_with_relations( study_root=study_root, - item=item, - previous_item=previous_item, - new_item=new_study_disease_milestone, + action_type=Delete, + before=previous_item, + after=new_study_disease_milestone, + exclude_relationships=exclude_relationships, + author_id=self.author_id, ) - manage_previous_connected_study_selection_relationships( - previous_item=previous_item, - study_value_node=study_value, - new_item=new_study_disease_milestone, - ) - return item - - def manage_versioning_create( - self, - study_root: StudyRoot, - item: StudyDiseaseMilestoneVO, - new_item: StudyDiseaseMilestone, - ): - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.status.value, - author_id=item.author_id, - ) - action.save() - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) - def manage_versioning_update( - self, - study_root: StudyRoot, - item: StudyDiseaseMilestoneVO, - previous_item: StudyDiseaseMilestone, - new_item: StudyDiseaseMilestone, - ): - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.status.value, - author_id=item.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_delete( - self, - study_root: StudyRoot, - item: StudyDiseaseMilestoneVO, - previous_item: StudyDiseaseMilestone, - new_item: StudyDiseaseMilestone, - ): - action = Delete( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.status.value, - author_id=item.author_id, - ) - action.save() - action.has_before.connect(previous_item) - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) + return item def get_distinct_headers( self, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_element_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_element_repository.py index ee784ba5..e6d4f55f 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_element_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_element_repository.py @@ -12,9 +12,10 @@ CTCodelistAttributesRepository, ) 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.controlled_terminology import ( + CTTermContext, CTTermRoot, ) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue @@ -312,6 +313,7 @@ def element_specific_has_connected_cell( ) return len(sdc_node) > 0 + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionElementAR, author_id: str) -> None: """ Persist the set of selected study element from the aggregate to the database @@ -370,19 +372,13 @@ def save(self, study_selection: StudySelectionElementAR, author_id: str) -> None audit_node = self._get_audit_node( study_selection, selection.study_selection_uid ) - # create the before node to the last_study_selection_node and audit trial to study_root - audit_node = self._set_before_audit_info( - audit_node=audit_node, - study_selection_node=last_study_selection_node, - study_root_node=study_root_node, - author_id=author_id, - ) # storage of the removed node audit trail to after put the "after" relationship to the new one audit_trail_nodes[selection.study_selection_uid] = audit_node # storage of the removed node to after get its connections last_nodes[selection.study_selection_uid] = last_study_selection_node if isinstance(audit_node, Delete): self._add_new_selection( + study_root_node, latest_study_value_node, order, selection, @@ -403,11 +399,8 @@ def save(self, study_selection: StudySelectionElementAR, author_id: str) -> None last_study_selection_node = last_nodes[selection.study_selection_uid] else: audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( + study_root_node, latest_study_value_node, order, selection, @@ -416,23 +409,9 @@ def save(self, study_selection: StudySelectionElementAR, author_id: str) -> None before_node=last_study_selection_node, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_selection_node: StudyElement, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @staticmethod def _add_new_selection( + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionElementVO, @@ -441,18 +420,19 @@ def _add_new_selection( before_node: StudyElement | None = None, ): # Create new element selection - study_element_selection_node = StudyElement(order=order).save() - study_element_selection_node.uid = selection.study_selection_uid - study_element_selection_node.accepted_version = selection.accepted_version - study_element_selection_node.name = selection.name - study_element_selection_node.short_name = selection.short_name - study_element_selection_node.element_code = selection.code - study_element_selection_node.description = selection.description - study_element_selection_node.planned_duration = selection.planned_duration - study_element_selection_node.start_rule = selection.start_rule - study_element_selection_node.end_rule = selection.end_rule - study_element_selection_node.element_colour = selection.element_colour - study_element_selection_node.save() + study_element_selection_node = StudyElement( + order=order, + uid=selection.study_selection_uid, + name=selection.name, + short_name=selection.short_name, + accepted_version=selection.accepted_version, + element_code=selection.code, + description=selection.description, + planned_duration=selection.planned_duration, + start_rule=selection.start_rule, + end_rule=selection.end_rule, + element_colour=selection.element_colour, + ).save() # Connect new node with study value if not for_deletion: @@ -460,16 +440,6 @@ def _add_new_selection( latest_study_value_node.has_study_element.connect( study_element_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_element_selection_node) - - if before_node is not None: - manage_previous_connected_study_selection_relationships( - previous_item=before_node, - study_value_node=latest_study_value_node, - new_item=study_element_selection_node, - exclude_study_selection_relationships=[], - ) # check if element subtype is set if selection.element_subtype_uid: @@ -486,6 +456,17 @@ def _add_new_selection( ) study_element_selection_node.element_subtype.connect(selected_term_node) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=study_element_selection_node, + exclude_relationships=[ + CTTermContext, + ], + author_id=selection.author_id, + ) + def generate_uid(self) -> str: return StudyElement.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_endpoint_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_endpoint_repository.py index 9187d9d8..d339eaec 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_endpoint_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_endpoint_repository.py @@ -1,4 +1,3 @@ -import datetime from typing import Any from neomodel import db @@ -10,8 +9,12 @@ from clinical_mdr_api.domain_repositories.controlled_terminologies.ct_codelist_attributes_repository import ( CTCodelistAttributesRepository, ) +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models.concepts import UnitDefinitionRoot from clinical_mdr_api.domain_repositories.models.controlled_terminology import ( + CTTermContext, CTTermRoot, ) from clinical_mdr_api.domain_repositories.models.generic import Conjunction @@ -22,11 +25,17 @@ Edit, StudyAction, ) -from clinical_mdr_api.domain_repositories.models.study_selections import StudyEndpoint +from clinical_mdr_api.domain_repositories.models.study_selections import ( + StudyEndpoint, + StudyObjective, +) from clinical_mdr_api.domain_repositories.models.syntax import ( EndpointRoot, EndpointTemplateRoot, + EndpointTemplateValue, + EndpointValue, TimeframeRoot, + TimeframeValue, ) from clinical_mdr_api.domain_repositories.models.template_parameter import ( TemplateParameter, @@ -283,6 +292,7 @@ def _get_audit_node( return Create() return Delete() + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionEndpointsAR, author_id: str) -> None: """ Persist the set of selected study endpoints from the aggregate to the database @@ -329,6 +339,8 @@ def save(self, study_selection: StudySelectionEndpointsAR, author_id: str) -> No # audit trail nodes dictionary, holds the new nodes created for the audit trail audit_trail_nodes = {} + # dictionary of last nodes to traverse to their old connections + last_nodes = {} # loop through and remove selections for order, selection in selections_to_remove: @@ -339,30 +351,35 @@ def save(self, study_selection: StudySelectionEndpointsAR, author_id: str) -> No audit_node = self._get_audit_node( study_selection, selection.study_selection_uid ) - audit_node = self._set_before_audit_info( - audit_node=audit_node, - study_selection_node=last_study_selection_node, - study_root_node=study_root_node, - author_id=author_id, - ) audit_trail_nodes[selection.study_selection_uid] = audit_node + last_nodes[selection.study_selection_uid] = last_study_selection_node if isinstance(audit_node, Delete): self._add_new_selection( - latest_study_value_node, order, selection, audit_node, True + study_root_node, + latest_study_value_node, + order, + selection, + audit_node, + True, + last_study_selection_node, ) # loop through and add selections for order, selection in selections_to_add: + last_study_selection_node = None if selection.study_selection_uid in audit_trail_nodes: audit_node = audit_trail_nodes[selection.study_selection_uid] + last_study_selection_node = last_nodes[selection.study_selection_uid] else: audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( - latest_study_value_node, order, selection, audit_node, False + study_root_node, + latest_study_value_node, + order, + selection, + audit_node, + False, + last_study_selection_node, ) # If some objectives already used this study endpoint @@ -413,34 +430,22 @@ def _remove_old_selection_if_exists( }, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_selection_node: StudyEndpoint, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - @staticmethod def _add_new_selection( + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionEndpointVO, audit_node: StudyAction, for_deletion: bool = False, + before_node: StudyEndpoint | None = None, ): # Create new endpoint selection - study_endpoint_selection_node = StudyEndpoint(order=order).save() - study_endpoint_selection_node.uid = selection.study_selection_uid - study_endpoint_selection_node.accepted_version = selection.accepted_version - study_endpoint_selection_node.save() + study_endpoint_selection_node = StudyEndpoint( + order=order, + uid=selection.study_selection_uid, + accepted_version=selection.accepted_version, + ).save() # Connect new node with StudyEndpoint template parameter _ = TemplateParameter.nodes.get(name=settings.study_endpoint_tp_name) @@ -463,8 +468,6 @@ def _add_new_selection( latest_study_value_node.has_study_endpoint.connect( study_endpoint_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_endpoint_selection_node) # check if endpoint is set if selection.endpoint_uid: @@ -566,6 +569,23 @@ def _add_new_selection( rel.position = 1 rel.save() + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=before_node, + after=study_endpoint_selection_node, + exclude_relationships=[ + EndpointValue, + EndpointTemplateValue, + TimeframeValue, + StudyObjective, + CTTermContext, + UnitDefinitionRoot, + Conjunction, + ], + author_id=selection.author_id, + ) + def is_used_as_parameter(self, study_selection_uid: str) -> bool: result = db.cypher_query( """ 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 5f725e4e..82e5c4e9 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 @@ -11,7 +11,7 @@ CTCodelistAttributesRepository, ) 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.controlled_terminology import ( CTTermRoot, @@ -33,7 +33,7 @@ from clinical_mdr_api.models.controlled_terminologies.ct_term import ( SimpleCTTermNameWithConflictFlag, ) -from common import queries +from common import exceptions, queries from common.auth.user import user from common.config import settings from common.exceptions import ValidationException @@ -433,13 +433,50 @@ def save(self, epoch: StudyEpochVO): return self._update(epoch, create=True) def _update(self, item: StudyEpochVO, create: bool = False): - study_root = StudyRoot.nodes.get(uid=item.study_uid) - study_value: StudyValue = study_root.latest_value.get_or_none() - ValidationException.raise_if( - study_value is None, msg="Study doesn't have draft version." - ) - if not create: - previous_item = study_value.has_study_epoch.get(uid=item.uid) + + # Fetch nodes referenced by uids + query = [ + "MATCH (study_root:StudyRoot {uid: $study_uid})-[:LATEST]->(latest_value:StudyValue)", + "MATCH (epoch:CTTermRoot {uid: $epoch_uid})", + "MATCH (epoch_subtype:CTTermRoot {uid: $epoch_subtype_uid})", + "MATCH (epoch_type:CTTermRoot {uid: $epoch_type_uid})", + ] + params = { + "study_uid": item.study_uid, + "epoch_uid": item.epoch.term_uid, + "epoch_subtype_uid": item.subtype.term_uid, + "epoch_type_uid": item.epoch_type.term_uid, + } + returns = [ + "study_root", + "latest_value", + "epoch", + "epoch_subtype", + "epoch_type", + ] + + if not create and item.uid: + query.append( + "MATCH (latest_value)-[:HAS_STUDY_EPOCH]->(study_epoch:StudyEpoch {uid: $study_epoch_uid})" + ) + params["study_epoch_uid"] = item.uid + returns.append("study_epoch") + + 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.BusinessLogicException( + msg=f"There should be one row returned with dependencies for StudyEpoch '{item.uid}'." + ) + + nodes = dict(zip(keys, results[0])) + study_root: StudyRoot = nodes["study_root"] + study_value: StudyValue = nodes["latest_value"] + epoch: CTTermRoot = nodes["epoch"] + epoch_subtype: CTTermRoot = nodes["epoch_subtype"] + epoch_type: CTTermRoot = nodes["epoch_type"] + previous_item: StudyEpoch | None = nodes.get("study_epoch") allow_removed_terms = not create @@ -462,10 +499,10 @@ def _update(self, item: StudyEpochVO, create: bool = False): item.uid = new_study_epoch.uid # connect to epoch subtype - ct_epoch_subtype = CTTermRoot.nodes.get(uid=item.subtype.term_uid) + # ct_epoch_subtype = CTTermRoot.nodes.get(uid=item.subtype.term_uid) selected_epoch_subtype_node = ( CTCodelistAttributesRepository().get_or_create_selected_term( - ct_epoch_subtype, + epoch_subtype, codelist_submission_value=settings.study_epoch_subtype_cl_submval, catalogue_name=settings.sdtm_ct_catalogue_name, allow_removed_terms=allow_removed_terms, @@ -474,10 +511,10 @@ def _update(self, item: StudyEpochVO, create: bool = False): new_study_epoch.has_epoch_subtype.connect(selected_epoch_subtype_node) # connect to epoch type - ct_epoch_type = CTTermRoot.nodes.get(uid=item.epoch_type.term_uid) + # ct_epoch_type = CTTermRoot.nodes.get(uid=item.epoch_type.term_uid) selected_epoch_type_node = ( CTCodelistAttributesRepository().get_or_create_selected_term( - ct_epoch_type, + epoch_type, codelist_submission_value=settings.study_epoch_type_cl_submval, catalogue_name=settings.sdtm_ct_catalogue_name, allow_removed_terms=allow_removed_terms, @@ -486,10 +523,10 @@ def _update(self, item: StudyEpochVO, create: bool = False): new_study_epoch.has_epoch_type.connect(selected_epoch_type_node) # connect to epoch - ct_epoch = CTTermRoot.nodes.get(uid=item.epoch.term_uid) + # ct_epoch = CTTermRoot.nodes.get(uid=item.epoch.term_uid) selected_epoch_node = ( CTCodelistAttributesRepository().get_or_create_selected_term( - ct_epoch, + epoch, codelist_submission_value=settings.study_epoch_cl_submval, catalogue_name=settings.sdtm_ct_catalogue_name, allow_removed_terms=allow_removed_terms, @@ -499,77 +536,38 @@ def _update(self, item: StudyEpochVO, create: bool = False): if create: new_study_epoch.study_value.connect(study_value) - self.manage_versioning_create( - study_root=study_root, item=item, new_item=new_study_epoch + _manage_versioning_with_relations( + study_root=study_root, + action_type=Create, + before=None, + after=new_study_epoch, + author_id=self.author_id, ) else: + exclude_relations = ( + StudyEpoch.has_epoch_type, + StudyEpoch.has_epoch_subtype, + StudyEpoch.has_epoch, + ) + # previous_item = study_value.has_study_epoch.get(uid=item.uid) if item.is_deleted: - self.manage_versioning_delete( + _manage_versioning_with_relations( study_root=study_root, - item=item, - # pylint: disable=possibly-used-before-assignment - previous_item=previous_item, - new_item=new_study_epoch, + action_type=Delete, + before=previous_item, + after=new_study_epoch, + exclude_relationships=exclude_relations, + author_id=self.author_id, ) else: new_study_epoch.study_value.connect(study_value) - self.manage_versioning_update( + _manage_versioning_with_relations( study_root=study_root, - item=item, - previous_item=previous_item, - new_item=new_study_epoch, + action_type=Edit, + before=previous_item, + after=new_study_epoch, + exclude_relationships=exclude_relations, + author_id=self.author_id, ) - manage_previous_connected_study_selection_relationships( - previous_item=previous_item, - study_value_node=study_value, - new_item=new_study_epoch, - exclude_study_selection_relationships=[], - ) return item - - def manage_versioning_create( - self, study_root: StudyRoot, item: StudyEpochVO, new_item: StudyEpoch - ): - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.status.value, - author_id=item.author_id, - ) - action.save() - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) - - def manage_versioning_update( - self, - study_root: StudyRoot, - item: StudyEpochVO, - previous_item: StudyEpoch, - new_item: StudyEpoch, - ): - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.status.value, - author_id=item.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_delete( - self, - study_root: StudyRoot, - item: StudyEpochVO, - previous_item: StudyEpoch, - new_item: StudyEpoch, - ): - action = Delete( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.status.value, - author_id=item.author_id, - ) - action.save() - action.has_before.connect(previous_item) - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_objective_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_objective_repository.py index 8ac123d9..4fab26fe 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_objective_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_objective_repository.py @@ -12,9 +12,10 @@ CTCodelistAttributesRepository, ) 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.controlled_terminology import ( + CTTermContext, CTTermRoot, ) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue @@ -28,6 +29,8 @@ from clinical_mdr_api.domain_repositories.models.syntax import ( ObjectiveRoot, ObjectiveTemplateRoot, + ObjectiveTemplateValue, + ObjectiveValue, ) from clinical_mdr_api.domains.study_selections.study_selection_objective import ( StudySelectionObjectivesAR, @@ -233,6 +236,7 @@ def _get_audit_node( return Create() return Delete() + # pylint: disable=unused-argument def save(self, study_selection: StudySelectionObjectivesAR, author_id: str) -> None: """ Persist the set of selected study objectives from the aggregate to the database @@ -288,18 +292,13 @@ def save(self, study_selection: StudySelectionObjectivesAR, author_id: str) -> N audit_node = self._get_audit_node( study_selection, study_objective.study_selection_uid ) - audit_node = self._set_before_audit_info( - audit_node=audit_node, - study_objective_selection_node=last_study_selection_node, - study_root_node=study_root_node, - author_id=author_id, - ) audit_trail_nodes[study_objective.study_selection_uid] = ( audit_node, last_study_selection_node, ) if isinstance(audit_node, Delete): self._add_new_selection( + study_root_node, latest_study_value_node, order, study_objective, @@ -317,11 +316,8 @@ def save(self, study_selection: StudySelectionObjectivesAR, author_id: str) -> N ] else: audit_node = Create() - audit_node.author_id = selection.author_id - audit_node.date = selection.start_date - audit_node.save() - study_root_node.audit_trail.connect(audit_node) self._add_new_selection( + study_root_node, latest_study_value_node, order, selection, @@ -330,23 +326,9 @@ def save(self, study_selection: StudySelectionObjectivesAR, author_id: str) -> N False, ) - @staticmethod - def _set_before_audit_info( - audit_node: StudyAction, - study_objective_selection_node: StudyObjective, - study_root_node: StudyRoot, - author_id: str, - ) -> StudyAction: - audit_node.author_id = author_id - audit_node.date = datetime.datetime.now(datetime.timezone.utc) - audit_node.save() - - audit_node.has_before.connect(study_objective_selection_node) - study_root_node.audit_trail.connect(audit_node) - return audit_node - def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySelectionObjectiveVO, @@ -355,17 +337,17 @@ def _add_new_selection( for_deletion: bool = False, ): # Create new objective selection - study_objective_selection_node = StudyObjective(order=order) - study_objective_selection_node.uid = selection.study_selection_uid - study_objective_selection_node.accepted_version = selection.accepted_version - study_objective_selection_node.save() + study_objective_selection_node = StudyObjective( + order=order, + uid=selection.study_selection_uid, + accepted_version=selection.accepted_version, + ).save() + if not for_deletion: # Connect new node with study value latest_study_value_node.has_study_objective.connect( study_objective_selection_node ) - # Connect new node with audit trail - audit_node.has_after.connect(study_objective_selection_node) # check if objective is set if selection.objective_uid: @@ -407,14 +389,18 @@ def _add_new_selection( study_objective_selection_node.has_objective_level.connect( selected_objective_level_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_objective_selection_node, - exclude_study_selection_relationships=[], - ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=last_study_selection_node, + after=study_objective_selection_node, + exclude_relationships=[ + ObjectiveValue, + ObjectiveTemplateValue, + CTTermContext, + ], + author_id=selection.author_id, + ) def study_objective_exists(self, study_objective_uid: str) -> bool: """ diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_footnote_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_footnote_repository.py index aa39f0c6..01fc635d 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_footnote_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_footnote_repository.py @@ -1,10 +1,12 @@ -import datetime from textwrap import dedent from typing import Any from neomodel import db from clinical_mdr_api import utils +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models.study import StudyRoot from clinical_mdr_api.domain_repositories.models.study_audit_trail import ( Create, @@ -707,29 +709,46 @@ def save(self, soa_footnote_vo: StudySoAFootnoteVO, create: bool = True): ) if create: - self.manage_versioning_create( + _manage_versioning_with_relations( study_root=study_root, - study_soa_footnote_vo=soa_footnote_vo, - new_node=soa_footnote_node, + action_type=Create, + before=None, + after=soa_footnote_node, + author_id=soa_footnote_vo.author_id, ) soa_footnote_node.study_value.connect(study_value) else: previous_item = StudySoAFootnote.nodes.filter(uid=soa_footnote_vo.uid).has( study_value=True, has_before=False )[0] + exclude_relationships = [ + FootnoteValue, + FootnoteTemplateValue, + StudySoAFootnote.references_study_activity, + StudySoAFootnote.references_study_activity_subgroup, + StudySoAFootnote.references_study_activity_group, + StudySoAFootnote.references_study_soa_group, + StudySoAFootnote.references_study_epoch, + StudySoAFootnote.references_study_visit, + StudySoAFootnote.references_study_activity_schedule, + ] if soa_footnote_vo.is_deleted: - self.manage_versioning_delete( + _manage_versioning_with_relations( study_root=study_root, - study_soa_footnote_vo=soa_footnote_vo, - previous_node=previous_item, - new_node=soa_footnote_node, + action_type=Delete, + before=previous_item, + after=soa_footnote_node, + exclude_relationships=exclude_relationships, + author_id=soa_footnote_vo.author_id, ) else: - self.manage_versioning_update( + _manage_versioning_with_relations( study_root=study_root, - study_soa_footnote_vo=soa_footnote_vo, - previous_node=previous_item, - new_node=soa_footnote_node, + action_type=Edit, + before=previous_item, + after=soa_footnote_node, + exclude_relationships=exclude_relationships, + author_id=soa_footnote_vo.author_id, ) soa_footnote_node.study_value.connect(study_value) # disconnect old StudyValue node to only keep StudyValue connection to the Latest value of StudySoAFootnote @@ -770,52 +789,6 @@ def get_all_versions(self, study_uid: str) -> list[StudySoAFootnoteVOHistory]: all_selections.append(selection_vo) return all_selections - def manage_versioning_create( - self, - study_root: StudyRoot, - study_soa_footnote_vo: StudySoAFootnoteVO, - new_node: StudySoAFootnote, - ): - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), - author_id=study_soa_footnote_vo.author_id, - ) - action.save() - action.has_after.connect(new_node) - study_root.audit_trail.connect(action) - - def manage_versioning_update( - self, - study_root: StudyRoot, - study_soa_footnote_vo: StudySoAFootnoteVO, - previous_node: StudySoAFootnote, - new_node: StudySoAFootnote, - ): - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), - author_id=study_soa_footnote_vo.author_id, - ) - action.save() - action.has_before.connect(previous_node) - action.has_after.connect(new_node) - study_root.audit_trail.connect(action) - - def manage_versioning_delete( - self, - study_root: StudyRoot, - study_soa_footnote_vo: StudySoAFootnoteVO, - previous_node: StudySoAFootnote, - new_node: StudySoAFootnote, - ): - action = Delete( - date=datetime.datetime.now(datetime.timezone.utc), - author_id=study_soa_footnote_vo.author_id, - ) - action.save() - action.has_before.connect(previous_node) - action.has_after.connect(new_node) - study_root.audit_trail.connect(action) - def check_exists_soa_footnotes_for_footnote_and_study_uid( self, study_uid: str, footnote_uid: str, soa_footnote_uid_to_exclude: str ) -> str | None: 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 da35216b..bd044e64 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 @@ -8,12 +8,12 @@ CTCodelistAttributesRepository, ) 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.controlled_terminology import ( CTTermRoot, ) -from clinical_mdr_api.domain_repositories.models.study import StudyValue +from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import StudyAction from clinical_mdr_api.domain_repositories.models.study_selections import ( StudySelection, @@ -187,6 +187,7 @@ def get_study_selection_node_from_latest_study_value( def _add_new_selection( self, + study_root: StudyRoot, latest_study_value_node: StudyValue, order: int, selection: StudySoAGroupVO, @@ -207,8 +208,6 @@ def _add_new_selection( # Connect new node with study value latest_study_value_node.has_study_soa_group.connect(study_soa_group_node) - # Connect new node with audit trail - audit_node.has_after.connect(study_soa_group_node) # Set flowchart group ct_term_root = CTTermRoot.nodes.get(uid=selection.soa_group_term_uid) selected_term_node = ( @@ -219,12 +218,14 @@ def _add_new_selection( ) ) study_soa_group_node.has_flowchart_group.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_soa_group_node, - ) + _manage_versioning_with_relations( + study_root=study_root, + action_type=type(audit_node), + before=last_study_selection_node, + after=study_soa_group_node, + exclude_relationships=[StudySoAGroup.has_flowchart_group], + author_id=selection.author_id, + ) def generate_uid(self) -> str: return StudySoAGroup.get_next_free_uid_and_increment_counter() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_source_variable_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_source_variable_repository.py index ac5991fc..b1e761fc 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_source_variable_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_source_variable_repository.py @@ -1,8 +1,9 @@ -import datetime - from clinical_mdr_api.domain_repositories._utils.helpers import ( acquire_write_lock_study_value, ) +from clinical_mdr_api.domain_repositories.generic_repository import ( + _manage_versioning_with_relations, +) from clinical_mdr_api.domain_repositories.models._utils import ListDistinct from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import Create, Edit @@ -81,14 +82,13 @@ def post_study_source_variable( } )[0] latest_study_value.has_study_source_variable.connect(study_source_variable_node) - - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), + _manage_versioning_with_relations( + study_root=study_root, + action_type=Create, + before=None, + after=study_source_variable_node, author_id=author_id, ) - action.save() - action.has_after.connect(study_source_variable_node) - study_root.audit_trail.connect(action) return self.get_study_source_variable(study_uid=study_uid) @@ -118,13 +118,13 @@ def edit_study_source_variable( )[0] latest_study_value.has_study_source_variable.connect(study_source_variable_node) - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), + _manage_versioning_with_relations( + study_root=study_root, + action_type=Edit, + before=previous_node, + after=study_source_variable_node, + exclude_relationships=[], author_id=author_id, ) - action.save() - action.has_before.connect(previous_node) - action.has_after.connect(study_source_variable_node) - study_root.audit_trail.connect(action) return self.get_study_source_variable(study_uid=study_uid) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_standard_version_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_standard_version_repository.py index 7bde7477..cc620641 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_standard_version_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_standard_version_repository.py @@ -1,11 +1,10 @@ -import datetime from typing import Any, Sequence, TypeVar from neomodel import Q from neomodel.sync_.match import Optional 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.controlled_terminology import CTPackage @@ -235,84 +234,39 @@ def _update(self, item: StudyStandardVersionVO, create: bool = False, delete=Fal new_study_standard_version.has_ct_package.connect(ct_package) if create: - self.manage_versioning_create( - study_root=study_root, item=item, new_item=new_study_standard_version + _manage_versioning_with_relations( + study_root=study_root, + action_type=Create, + before=None, + after=new_study_standard_version, + author_id=self.author_id, ) new_study_standard_version.study_value.connect(study_value) else: previous_item: StudyStandardVersion = ( study_value.has_study_standard_version.get(uid=item.uid) ) - + exclude_relationships = [CTPackage] if delete is False: # update - self.manage_versioning_update( + _manage_versioning_with_relations( study_root=study_root, - item=item, - previous_item=previous_item, - new_item=new_study_standard_version, + action_type=Edit, + before=previous_item, + after=new_study_standard_version, + exclude_relationships=exclude_relationships, + author_id=self.author_id, ) new_study_standard_version.study_value.connect(study_value) else: # delete - self.manage_versioning_delete( + _manage_versioning_with_relations( study_root=study_root, - item=item, - previous_item=previous_item, - new_item=new_study_standard_version, + action_type=Delete, + before=previous_item, + after=new_study_standard_version, + exclude_relationships=exclude_relationships, + author_id=self.author_id, ) - manage_previous_connected_study_selection_relationships( - previous_item=previous_item, - study_value_node=study_value, - new_item=new_study_standard_version, - ) - return item - - def manage_versioning_create( - self, - study_root: StudyRoot, - item: StudyStandardVersionVO, - new_item: StudyStandardVersion, - ): - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.study_status.value, - author_id=item.author_id, - ) - action.save() - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) - def manage_versioning_update( - self, - study_root: StudyRoot, - item: StudyStandardVersionVO, - previous_item: StudyStandardVersion, - new_item: StudyStandardVersion, - ): - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.study_status.value, - author_id=item.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_delete( - self, - study_root: StudyRoot, - item: StudyStandardVersionVO, - previous_item: StudyStandardVersion, - new_item: StudyStandardVersion, - ): - action = Delete( - date=datetime.datetime.now(datetime.timezone.utc), - status=item.study_status.value, - author_id=item.author_id, - ) - action.save() - action.has_before.connect(previous_item) - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) + return item 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 3b48ccc7..181b99f9 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 @@ -14,7 +14,7 @@ CTCodelistAttributesRepository, ) 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.concepts import ( StudyDayRoot, @@ -64,7 +64,7 @@ from clinical_mdr_api.models.controlled_terminologies.ct_term import ( SimpleCTTermNameWithConflictFlag, ) -from common import queries +from common import exceptions, queries from common.auth.user import user from common.config import settings from common.exceptions import ValidationException @@ -720,60 +720,124 @@ def get_all_versions( ) return extracted_items - def manage_versioning_create( - self, study_root: StudyRoot, study_visit: StudyVisitVO, new_item: StudyVisit - ): - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), - status=study_visit.status.value, - author_id=study_visit.author_id, - ) - action.save() - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) - - def manage_versioning_update( - self, - study_root: StudyRoot, - study_visit: StudyVisitVO, - previous_item: StudyVisit, - new_item: StudyVisit, - ): - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), - status=study_visit.status.value, - author_id=study_visit.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_delete( - self, - study_root: StudyRoot, - study_visit: StudyVisitVO, - previous_item: StudyVisit, - new_item: StudyVisit, - ): - action = Delete( - date=datetime.datetime.now(datetime.timezone.utc), - status=study_visit.status.value, - author_id=study_visit.author_id, - ) - action.save() - action.has_before.connect(previous_item) - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) - def _update(self, study_visit: StudyVisitVO, create: bool = False): - study_root: StudyRoot = StudyRoot.nodes.get(uid=study_visit.study_uid) - study_value: StudyValue = study_root.latest_value.get_or_none() - ValidationException.raise_if( - study_value is None, msg="Study doesn't have draft version." + + # Fetch nodes referenced by uids + query = [ + "MATCH (study_root:StudyRoot {uid: $study_uid})-[:LATEST]->(latest_value:StudyValue)", + "MATCH (latest_value)-[:HAS_STUDY_EPOCH]->(study_epoch:StudyEpoch {uid: $study_epoch_uid}) WHERE NOT (study_epoch)-[:BEFORE]-()", + "MATCH (visit_type:CTTermRoot {uid: $visit_type_uid})", + "MATCH (visit_contact_mode:CTTermRoot {uid: $visit_contact_mode_uid})", + "MATCH (visit_name:VisitNameRoot {uid: $visit_name_uid})", + ] + params = { + "study_uid": study_visit.study_uid, + "study_epoch_uid": study_visit.epoch_uid, + "visit_type_uid": study_visit.visit_type.term_uid, + "visit_contact_mode_uid": study_visit.visit_contact_mode.term_uid, + "visit_name_uid": study_visit.visit_name_sc.uid, + } + returns = [ + "study_root", + "latest_value", + "study_epoch", + "visit_type", + "visit_contact_mode", + "visit_name", + ] + + if study_visit.repeating_frequency: + query.append( + "MATCH (repeating_frequency:CTTermRoot {uid: $repeating_frequency_uid})" + ) + params["repeating_frequency_uid"] = study_visit.repeating_frequency.term_uid + returns.append("repeating_frequency") + if study_visit.epoch_allocation: + query.append( + "MATCH (epoch_allocation:CTTermRoot {uid: $epoch_allocation_uid})" + ) + params["epoch_allocation_uid"] = study_visit.epoch_allocation.term_uid + returns.append("epoch_allocation") + if study_visit.timepoint: + query.append("MATCH (timepoint:TimePointRoot {uid: $timepoint_uid})") + params["timepoint_uid"] = study_visit.timepoint.uid + returns.append("timepoint") + if study_visit.study_day: + query.append("MATCH (study_day:StudyDayRoot {uid: $study_day_uid})") + params["study_day_uid"] = study_visit.study_day.uid + returns.append("study_day") + if study_visit.study_duration_days: + query.append( + "MATCH (study_duration_days:StudyDurationDaysRoot {uid: $study_duration_days_uid})" + ) + params["study_duration_days_uid"] = study_visit.study_duration_days.uid + returns.append("study_duration_days") + if study_visit.study_week: + query.append("MATCH (study_week:StudyWeekRoot {uid: $study_week_uid})") + params["study_week_uid"] = study_visit.study_week.uid + returns.append("study_week") + if study_visit.study_duration_weeks: + query.append( + "MATCH (study_duration_weeks:StudyDurationWeeksRoot {uid: $study_duration_weeks_uid})" + ) + params["study_duration_weeks_uid"] = study_visit.study_duration_weeks.uid + returns.append("study_duration_weeks") + if study_visit.week_in_study: + query.append( + "MATCH (week_in_study:WeekInStudyRoot {uid: $week_in_study_uid})" + ) + params["week_in_study_uid"] = study_visit.week_in_study.uid + returns.append("week_in_study") + if study_visit.study_visit_group: + query.append( + "MATCH (study_visit_group:StudyVisitGroup {uid: $study_visit_group_uid})" + ) + params["study_visit_group_uid"] = study_visit.study_visit_group.uid + returns.append("study_visit_group") + if study_visit.window_unit_uid: + query.append( + "MATCH (window_unit:UnitDefinitionRoot {uid: $window_unit_uid})" + ) + params["window_unit_uid"] = study_visit.window_unit_uid + returns.append("window_unit") + + if not create and study_visit.uid: + query.append( + "MATCH (latest_value)-[:HAS_STUDY_VISIT]->(study_visit:StudyVisit {uid: $study_visit_uid})" + ) + params["study_visit_uid"] = study_visit.uid + returns.append("study_visit") + + 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.BusinessLogicException( + msg=f"There should be one row returned with dependencies for StudyVisit '{study_visit.uid}'." + ) + + nodes = dict(zip(keys, results[0])) + study_root: StudyRoot = nodes["study_root"] + study_value: StudyValue = nodes["latest_value"] + study_epoch: StudyEpoch = nodes["study_epoch"] + visit_type: CTTermRoot = nodes["visit_type"] + visit_contact_mode: CTTermRoot = nodes["visit_contact_mode"] + repeating_frequency: CTTermRoot | None = nodes.get("repeating_frequency") + epoch_allocation: CTTermRoot | None = nodes.get("epoch_allocation") + timepoint: TimePointRoot | None = nodes.get("timepoint") + study_day: StudyDayRoot | None = nodes.get("study_day") + study_duration_days: StudyDurationDaysRoot | None = nodes.get( + "study_duration_days" ) - if not create: - previous_item = study_value.has_study_visit.get(uid=study_visit.uid) + study_week: StudyWeekRoot | None = nodes.get("study_week") + study_duration_weeks: StudyDurationWeeksRoot | None = nodes.get( + "study_duration_weeks" + ) + week_in_study: WeekInStudyRoot | None = nodes.get("week_in_study") + study_visit_group: StudyVisitGroup | None = nodes.get("study_visit_group") + visit_name: VisitNameRoot = nodes["visit_name"] + window_unit: UnitDefinitionRoot | None = nodes.get("window_unit") + previous_item: StudyVisit | None = nodes.get("study_visit") new_visit = StudyVisit( uid=study_visit.uid, @@ -802,7 +866,6 @@ def _update(self, study_visit: StudyVisitVO, create: bool = False): study_visit.uid = new_visit.uid # Visit type - visit_type = CTTermRoot.nodes.get(uid=study_visit.visit_type.term_uid) selected_visit_type_node = ( CTCodelistAttributesRepository().get_or_create_selected_term( visit_type, @@ -812,13 +875,20 @@ def _update(self, study_visit: StudyVisitVO, create: bool = False): ) new_visit.has_visit_type.connect(selected_visit_type_node) + # Visit contact mode + selected_contact_mode_node = ( + CTCodelistAttributesRepository().get_or_create_selected_term( + visit_contact_mode, + codelist_submission_value=settings.study_visit_contact_mode_cl_submval, + catalogue_name=settings.sdtm_ct_catalogue_name, + ) + ) + new_visit.has_visit_contact_mode.connect(selected_contact_mode_node) + # Repeating visit frequency if study_visit.repeating_frequency: - visit_repeating_frequency = CTTermRoot.nodes.get( - uid=study_visit.repeating_frequency.term_uid - ) selected_repeating_frequency_node = CTCodelistAttributesRepository().get_or_create_selected_term( - visit_repeating_frequency, + repeating_frequency, codelist_submission_value=settings.repeating_visit_frequency_cl_submval, catalogue_name=settings.sdtm_ct_catalogue_name, ) @@ -826,78 +896,40 @@ def _update(self, study_visit: StudyVisitVO, create: bool = False): # Time point if study_visit.timepoint: - visit_timepoint = TimePointRoot.nodes.get(uid=study_visit.timepoint.uid) - new_visit.has_timepoint.connect(visit_timepoint) + new_visit.has_timepoint.connect(timepoint) + # Study day if study_visit.study_day: - study_day_numeric_value = StudyDayRoot.nodes.get( - uid=study_visit.study_day.uid - ) - new_visit.has_study_day.connect(study_day_numeric_value) + new_visit.has_study_day.connect(study_day) # Study duration days if study_visit.study_duration_days: - study_duration_days = StudyDurationDaysRoot.nodes.get( - uid=study_visit.study_duration_days.uid - ) new_visit.has_study_duration_days.connect(study_duration_days) # Study week if study_visit.study_week: - study_week_numeric_value = StudyWeekRoot.nodes.get( - uid=study_visit.study_week.uid - ) - new_visit.has_study_week.connect(study_week_numeric_value) + new_visit.has_study_week.connect(study_week) # Study duration weeks if study_visit.study_duration_weeks: - study_duration_weeks = StudyDurationWeeksRoot.nodes.get( - uid=study_visit.study_duration_weeks.uid - ) new_visit.has_study_duration_weeks.connect(study_duration_weeks) # Week in study if study_visit.week_in_study: - week_in_study = WeekInStudyRoot.nodes.get(uid=study_visit.week_in_study.uid) new_visit.has_week_in_study.connect(week_in_study) if study_visit.study_visit_group: - study_visit_group = StudyVisitGroup.nodes.get( - uid=study_visit.study_visit_group.uid - ) new_visit.in_visit_group.connect(study_visit_group) # Visit name - visit_name_text_value = VisitNameRoot.nodes.get( - uid=study_visit.visit_name_sc.uid - ) - new_visit.has_visit_name.connect(visit_name_text_value) + new_visit.has_visit_name.connect(visit_name) # Visit window time unit if study_visit.window_unit_uid is not None: - window_unit = UnitDefinitionRoot.nodes.get(uid=study_visit.window_unit_uid) new_visit.has_window_unit.connect(window_unit) - else: - window_unit = None - - # Visit contact mode - visit_contact_mode = CTTermRoot.nodes.get( - uid=study_visit.visit_contact_mode.term_uid - ) - selected_contact_mode_node = ( - CTCodelistAttributesRepository().get_or_create_selected_term( - visit_contact_mode, - codelist_submission_value=settings.study_visit_contact_mode_cl_submval, - catalogue_name=settings.sdtm_ct_catalogue_name, - ) - ) - new_visit.has_visit_contact_mode.connect(selected_contact_mode_node) # Epoch allocation if study_visit.epoch_allocation: - epoch_allocation = CTTermRoot.nodes.get( - uid=study_visit.epoch_allocation.term_uid - ) selected_epoch_allocation_node = ( CTCodelistAttributesRepository().get_or_create_selected_term( epoch_allocation, @@ -907,41 +939,51 @@ def _update(self, study_visit: StudyVisitVO, create: bool = False): ) new_visit.has_epoch_allocation.connect(selected_epoch_allocation_node) - study_epoch = ( - StudyEpoch.nodes.has(has_after=True) - .has(has_before=False) - .get(uid=study_visit.epoch_uid) - ) new_visit.study_epoch_has_study_visit.connect(study_epoch) if not create: + exclude_study_selection_relationships = [ + StudyEpoch, + TimePointRoot, + StudyDayRoot, + StudyDurationDaysRoot, + StudyWeekRoot, + StudyDurationWeeksRoot, + WeekInStudyRoot, + VisitNameRoot, + StudyVisitGroup, + StudyVisit.has_visit_type, + StudyVisit.has_repeating_frequency, + StudyVisit.has_visit_contact_mode, + StudyVisit.has_epoch_allocation, + StudyVisit.has_window_unit, + ] if study_visit.is_deleted: - self.manage_versioning_delete( + _manage_versioning_with_relations( study_root=study_root, - study_visit=study_visit, - previous_item=previous_item, - new_item=new_visit, + action_type=Delete, + before=previous_item, + after=new_visit, + exclude_relationships=exclude_study_selection_relationships, + author_id=self.author_id, ) else: new_visit.has_study_visit.connect(study_value) - self.manage_versioning_update( + _manage_versioning_with_relations( study_root=study_root, - study_visit=study_visit, - previous_item=previous_item, - new_item=new_visit, + action_type=Edit, + before=previous_item, + after=new_visit, + exclude_relationships=exclude_study_selection_relationships, + author_id=self.author_id, ) - exclude_study_selection_relationships = [ - StudyEpoch, - ] - manage_previous_connected_study_selection_relationships( - previous_item=previous_item, - study_value_node=study_value, - new_item=new_visit, - exclude_study_selection_relationships=exclude_study_selection_relationships, - ) else: new_visit.has_study_visit.connect(study_value) - self.manage_versioning_create( - study_root=study_root, study_visit=study_visit, new_item=new_visit + _manage_versioning_with_relations( + study_root=study_root, + action_type=Create, + before=None, + after=new_visit, + author_id=self.author_id, ) return study_visit 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 7f1a2f8c..77dae5a7 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 @@ -29,6 +29,7 @@ class ActivityItemVO: activity_item_class_name: str | None ct_terms: list[CTTermItem] unit_definitions: list[CompactUnitDefinition] + text_value: str | None = None @classmethod def from_repository_values( @@ -38,6 +39,7 @@ def from_repository_values( activity_item_class_name: str | None, ct_terms: list[CTTermItem], unit_definitions: list[CompactUnitDefinition], + text_value: str | None = None, ) -> Self: activity_item_vo = cls( is_adam_param_specific=is_adam_param_specific, @@ -45,6 +47,7 @@ def from_repository_values( activity_item_class_name=activity_item_class_name, ct_terms=ct_terms, unit_definitions=unit_definitions, + text_value=text_value, ) return activity_item_vo diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/condition.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/condition.py index f3f78b07..559048aa 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/condition.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/condition.py @@ -9,8 +9,8 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmFormalExpressionModel, + OdmTranslatedTextModel, ) from common.exceptions import AlreadyExistsException @@ -19,7 +19,7 @@ class OdmConditionVO(ConceptVO): oid: str | None formal_expressions: list[OdmFormalExpressionModel] - descriptions: list[OdmDescriptionModel] + translated_texts: list[OdmTranslatedTextModel] aliases: list[OdmAliasModel] @classmethod @@ -28,14 +28,14 @@ def from_repository_values( oid: str | None, name: str, formal_expressions: list[OdmFormalExpressionModel], - descriptions: list[OdmDescriptionModel], + translated_texts: list[OdmTranslatedTextModel], aliases: list[OdmAliasModel], ) -> Self: return cls( oid=oid, name=name, formal_expressions=formal_expressions, - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, name_sentence_case=None, definition=None, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/form.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/form.py index 50e0796d..bdce846a 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/form.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/form.py @@ -9,7 +9,7 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmTranslatedTextModel, ) from common.exceptions import AlreadyExistsException from common.utils import booltostr @@ -20,7 +20,7 @@ class OdmFormVO(ConceptVO): oid: str | None repeating: str | int | None sdtm_version: str | None - descriptions: list[OdmDescriptionModel] + translated_texts: list[OdmTranslatedTextModel] aliases: list[OdmAliasModel] item_group_uids: list[str] vendor_attribute_uids: list[str] @@ -34,7 +34,7 @@ def from_repository_values( name: str, sdtm_version: str | None, repeating: str | int | None, - descriptions: list[OdmDescriptionModel], + translated_texts: list[OdmTranslatedTextModel], aliases: list[OdmAliasModel], item_group_uids: list[str], vendor_element_uids: list[str], @@ -46,7 +46,7 @@ def from_repository_values( name=name, sdtm_version=sdtm_version, repeating=repeating, - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, item_group_uids=item_group_uids, vendor_element_uids=vendor_element_uids, @@ -163,6 +163,7 @@ def edit_draft( class OdmFormRefVO: uid: str name: str + oid: str version: str study_event_uid: str order_number: int @@ -175,6 +176,7 @@ def from_repository_values( cls, uid: str, name: str, + oid: str, version: str, study_event_uid: str, order_number: int, @@ -185,6 +187,7 @@ def from_repository_values( return cls( uid=uid, name=name, + oid=oid, version=version, study_event_uid=study_event_uid, order_number=order_number, 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 323d44ad..f47eadb4 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 @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Any, Callable, Self +from typing import TYPE_CHECKING, Any, Callable, Self from clinical_mdr_api.domains.concepts.concept_base import ConceptVO from clinical_mdr_api.domains.concepts.odms.odm_ar_base import OdmARBase @@ -13,12 +13,15 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmTranslatedTextModel, ) from clinical_mdr_api.models.utils import GenericFilteringReturn from common.exceptions import AlreadyExistsException, BusinessLogicException from common.utils import booltostr +if TYPE_CHECKING: + from clinical_mdr_api.models.concepts.odms.odm_item import OdmItemCodelist + @dataclass(frozen=True) class OdmItemVO(ConceptVO): @@ -31,10 +34,10 @@ class OdmItemVO(ConceptVO): sds_var_name: str | None origin: str | None comment: str | None - descriptions: list[OdmDescriptionModel] + translated_texts: list[OdmTranslatedTextModel] aliases: list[OdmAliasModel] unit_definition_uids: list[str] - codelist_uid: str | None + codelist: "OdmItemCodelist | None" term_uids: list[str] vendor_attribute_uids: list[str] vendor_element_uids: list[str] @@ -54,10 +57,10 @@ def from_repository_values( sds_var_name: str | None, origin: str | None, comment: str | None, - descriptions: list[OdmDescriptionModel], + translated_texts: list[OdmTranslatedTextModel], aliases: list[OdmAliasModel], unit_definition_uids: list[str], - codelist_uid: str | None, + codelist: "OdmItemCodelist | None", term_uids: list[str], vendor_element_uids: list[str], vendor_attribute_uids: list[str], @@ -75,10 +78,10 @@ def from_repository_values( sds_var_name=sds_var_name, origin=origin, comment=comment, - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, unit_definition_uids=unit_definition_uids, - codelist_uid=codelist_uid, + codelist=codelist, term_uids=term_uids, vendor_element_uids=vendor_element_uids, vendor_attribute_uids=vendor_attribute_uids, @@ -107,7 +110,7 @@ def validate( data = { "library_name": library_name, "unit_definition_uids": self.unit_definition_uids, - "codelist_uid": self.codelist_uid, + "codelist_uid": self.codelist.uid if self.codelist else None, "term_uids": self.term_uids, "name": self.name, "oid": self.oid, @@ -137,27 +140,26 @@ def validate( "ODM Item", ) - BusinessLogicException.raise_if( - self.codelist_uid is not None - and not find_codelist_attribute_callback(self.codelist_uid), - msg=f"ODM Item tried to connect to non-existent Codelist with UID '{self.codelist_uid}'.", - ) + if self.codelist is not None and not find_codelist_attribute_callback( + self.codelist.uid + ): + raise BusinessLogicException( + msg=f"ODM Item tried to connect to non-existent Codelist with UID '{self.codelist.uid}'.", + ) if self.term_uids: - BusinessLogicException.raise_if_not( - self.codelist_uid, - msg="To add terms you need to specify a codelist.", - ) + if self.codelist is None: + raise BusinessLogicException( + msg="To add terms you need to specify a codelist." + ) - codelist_term_uids = ( - [term.uid for term in find_all_terms_callback(self.codelist_uid).items] - if self.codelist_uid is not None - else [] - ) + codelist_term_uids = [ + term.uid for term in find_all_terms_callback(self.codelist.uid).items + ] for term_uid in self.term_uids: BusinessLogicException.raise_if( term_uid not in codelist_term_uids, - msg=f"Term with UID '{term_uid}' doesn't belong to the specified Codelist with UID '{self.codelist_uid}'.", + msg=f"Term with UID '{term_uid}' doesn't belong to the specified Codelist with UID '{self.codelist.uid}'.", ) if self.activity_instances: diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item_group.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item_group.py index fe8517f4..a98856c0 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item_group.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item_group.py @@ -12,7 +12,7 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmTranslatedTextModel, ) from common.exceptions import AlreadyExistsException, BusinessLogicException from common.utils import booltostr @@ -27,7 +27,7 @@ class OdmItemGroupVO(ConceptVO): origin: str | None purpose: str | None comment: str | None - descriptions: list[OdmDescriptionModel] + translated_texts: list[OdmTranslatedTextModel] aliases: list[OdmAliasModel] sdtm_domain_uids: list[str] item_uids: list[str] @@ -46,7 +46,7 @@ def from_repository_values( origin: str | None, purpose: str | None, comment: str | None, - descriptions: list[OdmDescriptionModel], + translated_texts: list[OdmTranslatedTextModel], aliases: list[OdmAliasModel], sdtm_domain_uids: list[str], item_uids: list[str], @@ -63,7 +63,7 @@ def from_repository_values( origin=origin, purpose=purpose, comment=comment, - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, sdtm_domain_uids=sdtm_domain_uids, item_uids=item_uids, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/method.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/method.py index 2f19996b..a90c3078 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/method.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/method.py @@ -9,8 +9,8 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmFormalExpressionModel, + OdmTranslatedTextModel, ) from common.exceptions import AlreadyExistsException @@ -20,7 +20,7 @@ class OdmMethodVO(ConceptVO): oid: str | None method_type: str | None formal_expressions: list[OdmFormalExpressionModel] - descriptions: list[OdmDescriptionModel] + translated_texts: list[OdmTranslatedTextModel] aliases: list[OdmAliasModel] @classmethod @@ -30,7 +30,7 @@ def from_repository_values( name: str, method_type: str | None, formal_expressions: list[OdmFormalExpressionModel], - descriptions: list[OdmDescriptionModel], + translated_texts: list[OdmTranslatedTextModel], aliases: list[OdmAliasModel], ) -> Self: return cls( @@ -38,7 +38,7 @@ def from_repository_values( name=name, method_type=method_type, formal_expressions=formal_expressions, - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, name_sentence_case=None, definition=None, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/odm_xml_definition.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/odm_xml_definition.py index b35dfdc0..29a011c0 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/odm_xml_definition.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/odm_xml_definition.py @@ -120,10 +120,34 @@ class Question: translated_text: list[TranslatedText] +@dataclass +class OsbDesignNotes: + translated_text: list[TranslatedText] + _custom_element_name = "osb:DesignNotes" + + +@dataclass +class OsbCompletionInstructions: + translated_text: list[TranslatedText] + _custom_element_name = "osb:CompletionInstructions" + + +@dataclass +class OsbDisplayText: + translated_text: list[TranslatedText] + _custom_element_name = "osb:DisplayText" + + @dataclass class CodeListRef: codelist_oid: Attribute + def __init__(self, codelist_oid: Attribute, **kwargs): + self.codelist_oid = codelist_oid + + for key, val in kwargs.items(): + setattr(self, key, val) + @dataclass class MeasurementUnitRef: @@ -146,6 +170,9 @@ class ConditionDef: oid: Attribute name: Attribute description: Description + osb_design_notes: OsbDesignNotes | None + osb_completion_instructions: OsbCompletionInstructions | None + osb_display_text: OsbDisplayText | None aliases: list[Alias] formal_expressions: list[FormalExpression] @@ -154,6 +181,9 @@ def __init__( oid: Attribute, name: Attribute, description: Description, + osb_design_notes: OsbDesignNotes | None, + osb_completion_instructions: OsbCompletionInstructions | None, + osb_display_text: OsbDisplayText | None, aliases: list[Alias], formal_expressions: list[FormalExpression], **kwargs, @@ -161,11 +191,23 @@ def __init__( self.oid = oid self.name = name self.description = description + self.osb_design_notes = osb_design_notes + self.osb_completion_instructions = osb_completion_instructions + self.osb_display_text = osb_display_text self.aliases = aliases self.formal_expressions = formal_expressions if not self.description.translated_text: del self.description + if not self.osb_design_notes or not self.osb_design_notes.translated_text: + del self.osb_design_notes + if ( + not self.osb_completion_instructions + or not self.osb_completion_instructions.translated_text + ): + del self.osb_completion_instructions + if not self.osb_display_text or not self.osb_display_text.translated_text: + del self.osb_display_text for key, val in kwargs.items(): setattr(self, key, val) @@ -176,6 +218,9 @@ class MethodDef: name: Attribute type: Attribute description: Description + osb_design_notes: OsbDesignNotes | None + osb_completion_instructions: OsbCompletionInstructions | None + osb_display_text: OsbDisplayText | None aliases: list[Alias] formal_expressions: list[FormalExpression] @@ -185,6 +230,9 @@ def __init__( name: Attribute, method_type: Attribute, description: Description, + osb_design_notes: OsbDesignNotes | None, + osb_completion_instructions: OsbCompletionInstructions | None, + osb_display_text: OsbDisplayText | None, aliases: list[Alias], formal_expressions: list[FormalExpression], **kwargs, @@ -193,16 +241,37 @@ def __init__( self.name = name self.type = method_type self.description = description + self.osb_design_notes = osb_design_notes + self.osb_completion_instructions = osb_completion_instructions + self.osb_display_text = osb_display_text self.aliases = aliases self.formal_expressions = formal_expressions if not self.description.translated_text: del self.description + if not self.osb_design_notes or not self.osb_design_notes.translated_text: + del self.osb_design_notes + if ( + not self.osb_completion_instructions + or not self.osb_completion_instructions.translated_text + ): + del self.osb_completion_instructions + if not self.osb_display_text or not self.osb_display_text.translated_text: + del self.osb_display_text for key, val in kwargs.items(): setattr(self, key, val) +@dataclass +class OsbActivityInstance: + _string: str + adam_code: Attribute + topic_code: Attribute + is_derived: Attribute + _custom_element_name: str = "osb:ActivityInstance" + + class ItemDef: oid: Attribute name: Attribute @@ -214,7 +283,11 @@ class ItemDef: sds_var_name: Attribute question: Question description: Description + osb_design_notes: OsbDesignNotes | None + osb_completion_instructions: OsbCompletionInstructions | None + osb_display_text: OsbDisplayText | None aliases: list[Alias] + activity_instances: list[OsbActivityInstance] codelist_ref: CodeListRef measurement_unit_refs: list[MeasurementUnitRef] @@ -230,7 +303,11 @@ def __init__( sds_var_name: Attribute, question: Question, description: Description, + osb_design_notes: OsbDesignNotes | None, + osb_completion_instructions: OsbCompletionInstructions | None, + osb_display_text: OsbDisplayText | None, aliases: list[Alias], + activity_instances: list[OsbActivityInstance], codelist_ref: CodeListRef, measurement_unit_refs: list[MeasurementUnitRef], **kwargs, @@ -245,7 +322,12 @@ def __init__( self.sds_var_name = sds_var_name self.question = question self.description = description + self.osb_design_notes = osb_design_notes + self.osb_completion_instructions = osb_completion_instructions + self.osb_design_notes = osb_design_notes + self.osb_display_text = osb_display_text self.aliases = aliases + self.activity_instances = activity_instances self.codelist_ref = codelist_ref self.measurement_unit_refs = measurement_unit_refs @@ -255,6 +337,15 @@ def __init__( del self.description if not self.question.translated_text: del self.question + if not self.osb_design_notes or not self.osb_design_notes.translated_text: + del self.osb_design_notes + if ( + not self.osb_completion_instructions + or not self.osb_completion_instructions.translated_text + ): + del self.osb_completion_instructions + if not self.osb_display_text or not self.osb_display_text.translated_text: + del self.osb_display_text for key, val in kwargs.items(): setattr(self, key, val) @@ -308,6 +399,9 @@ class ItemGroupDef: domain: Attribute osb_domain_colors: list[OsbDomainColor] description: Description + osb_design_notes: OsbDesignNotes | None + osb_completion_instructions: OsbCompletionInstructions | None + osb_display_text: OsbDisplayText | None aliases: list[Alias] item_refs: list[ItemRef] @@ -321,6 +415,9 @@ def __init__( domain: Attribute, osb_domain_colors: list[OsbDomainColor], description: Description, + osb_design_notes: OsbDesignNotes | None, + osb_completion_instructions: OsbCompletionInstructions | None, + osb_display_text: OsbDisplayText | None, aliases: list[Alias], item_refs: list[ItemRef], **kwargs, @@ -333,11 +430,23 @@ def __init__( self.domain = domain self.osb_domain_colors = osb_domain_colors self.description = description + self.osb_design_notes = osb_design_notes + self.osb_completion_instructions = osb_completion_instructions + self.osb_display_text = osb_display_text self.aliases = aliases self.item_refs = item_refs if not self.description.translated_text: del self.description + if not self.osb_design_notes or not self.osb_design_notes.translated_text: + del self.osb_design_notes + if ( + not self.osb_completion_instructions + or not self.osb_completion_instructions.translated_text + ): + del self.osb_completion_instructions + if not self.osb_display_text or not self.osb_display_text.translated_text: + del self.osb_display_text for key, val in kwargs.items(): setattr(self, key, val) @@ -374,6 +483,9 @@ class FormDef: name: Attribute repeating: Attribute description: Description + osb_design_notes: OsbDesignNotes | None + osb_completion_instructions: OsbCompletionInstructions | None + osb_display_text: OsbDisplayText | None aliases: list[Alias] item_group_refs: list[ItemGroupRef] @@ -383,6 +495,9 @@ def __init__( name: Attribute, repeating: Attribute, description: Description, + osb_design_notes: OsbDesignNotes | None, + osb_completion_instructions: OsbCompletionInstructions | None, + osb_display_text: OsbDisplayText | None, aliases: list[Alias], item_group_refs: list[ItemGroupRef], **kwargs, @@ -391,11 +506,23 @@ def __init__( self.name = name self.repeating = repeating self.description = description + self.osb_design_notes = osb_design_notes + self.osb_completion_instructions = osb_completion_instructions + self.osb_display_text = osb_display_text self.aliases = aliases self.item_group_refs = item_group_refs if not self.description.translated_text: del self.description + if not self.osb_design_notes or not self.osb_design_notes.translated_text: + del self.osb_design_notes + if ( + not self.osb_completion_instructions + or not self.osb_completion_instructions.translated_text + ): + del self.osb_completion_instructions + if not self.osb_display_text or not self.osb_display_text.translated_text: + del self.osb_display_text for key, val in kwargs.items(): setattr(self, key, val) diff --git a/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_attributes.py b/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_attributes.py index cfe1a744..e15e9821 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_attributes.py @@ -40,7 +40,7 @@ class CTCodelistAttributesVO: preferred_term: str | None definition: str extensible: bool - ordinal: bool + is_ordinal: bool @classmethod def from_repository_values( @@ -53,7 +53,7 @@ def from_repository_values( preferred_term: str | None, definition: str, extensible: bool, - ordinal: bool, + is_ordinal: bool, ) -> Self: if child_codelist_uids is None: child_codelist_uids = [] @@ -66,7 +66,7 @@ def from_repository_values( preferred_term=preferred_term, definition=definition, extensible=extensible, - ordinal=ordinal, + is_ordinal=is_ordinal, ) return ct_codelist_attribute_vo @@ -81,7 +81,7 @@ def from_input_values( preferred_term: str | None, definition: str, extensible: bool, - ordinal: bool, + is_ordinal: bool, catalogue_exists_callback: Callable[[str], bool], codelist_exists_by_uid_callback: Callable[[str], bool] = lambda _: False, codelist_exists_by_name_callback: Callable[[str], bool] = lambda _: False, @@ -125,7 +125,7 @@ def from_input_values( preferred_term=preferred_term, definition=definition, extensible=extensible, - ordinal=bool(ordinal), + is_ordinal=bool(is_ordinal), ) return ct_codelist_attribute_vo diff --git a/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_term.py b/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_term.py index 4bbb231d..fe18762b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_term.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_codelist_term.py @@ -23,6 +23,7 @@ class CTCodelistTermVO: attributes_date: datetime submission_value: str order: int | None + ordinal: float | None concept_id: str | None nci_preferred_name: str | None definition: str @@ -42,6 +43,7 @@ def from_result_dict(cls, result: dict[str, Any]) -> Self: attributes_date=convert_to_datetime(result["attributes_date"]), submission_value=result["submission_value"], order=result.get("order"), + ordinal=result.get("ordinal"), start_date=convert_to_datetime(result["start_date"]), end_date=convert_to_datetime(result.get("end_date")), concept_id=result.get("concept_id"), @@ -87,6 +89,7 @@ class CTSimpleCodelistTermVO: submission_value: str preferred_term: str | None order: int | None + ordinal: float | None codelist_uid: str | None codelist_name: str | None codelist_submission_value: str | None @@ -100,6 +103,7 @@ def from_result_dict(cls, result: dict[str, Any]) -> Self: submission_value=result["submission_value"], preferred_term=result.get("preferred_term"), order=result.get("order"), + ordinal=result.get("ordinal"), codelist_uid=result.get("codelist_uid"), codelist_name=result.get("codelist_name"), codelist_submission_value=result.get("codelist_submission_value"), diff --git a/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_term_name.py b/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_term_name.py index 2d8c65a0..54c0ee89 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_term_name.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/controlled_terminologies/ct_term_name.py @@ -26,6 +26,7 @@ class CTTermCodelistVO: codelist_submission_value: str library_name: str codelist_concept_id: str | None = None + ordinal: float | None = None @dataclass(frozen=True) diff --git a/clinical-mdr-api/clinical_mdr_api/domains/enums.py b/clinical-mdr-api/clinical_mdr_api/domains/enums.py index 89f1a4c0..d293f15f 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/enums.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/enums.py @@ -35,3 +35,11 @@ class StudySourceVariableEnum(Enum): COHORT = "Cohort" SUBGROUP = "Subgroup" STRATUM = "Stratum" + + +class OdmTranslatedTextTypeEnum(Enum): + DESCRIPTION = "Description" + QUESTION = "Question" + OSB_DISPLAY_TEXT = "osb:DisplayText" + OSB_DESIGN_NOTES = "osb:DesignNotes" + OSB_COMPLETION_INSTRUCTIONS = "osb:CompletionInstructions" 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 a1e90774..a9f5aea5 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/iso_languages.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/iso_languages.py @@ -23,7 +23,7 @@ _639_2B = "639-2/B" _639_3 = "639-3" -LANGUAGES = [ +LANGUAGES: list[dict[str, Any]] = [ { _NAMES: ["Abkhazian"], _639_1: "ab", @@ -1902,6 +1902,9 @@ LANGUAGES_BY_639_3 = { key.casefold(): lang for lang in LANGUAGES for key in lang[_639_3] } +LANGUAGES_BY_NAME_AND_639_1_AND_639_2T = { + lang["names"][0]: (lang[_639_1], lang[_639_2T]) for lang in LANGUAGES +} LANGUAGES_INDEXED_BY: dict[str, dict[Any, dict[str, Any]]] = { _NAMES: LANGUAGES_BY_NAMES, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset.py b/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset.py index 16950759..160ed92b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset.py @@ -1,6 +1,6 @@ import datetime from dataclasses import dataclass -from typing import Self +from typing import Any, Self from clinical_mdr_api.domains.versioned_object_aggregate import ( LibraryItemAggregateRootBase, @@ -41,6 +41,7 @@ class SponsorModelDatasetVO: state: str | None extended_domain: str | None target_data_model_catalogue: str | None = None + extra_properties: dict[str, Any] | None = None @classmethod def from_repository_values( @@ -70,6 +71,7 @@ def from_repository_values( state: str | None, extended_domain: str | None, target_data_model_catalogue: str | None = None, + extra_properties: dict[str, Any] | None = None, ) -> Self: sponsor_model_dataset_vo = cls( sponsor_model_name=sponsor_model_name, @@ -97,6 +99,7 @@ def from_repository_values( state=state, extended_domain=extended_domain, target_data_model_catalogue=target_data_model_catalogue, + extra_properties=extra_properties or {}, ) return sponsor_model_dataset_vo diff --git a/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset_variable.py b/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset_variable.py index fe0256d7..8bd3dcf3 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset_variable.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/standard_data_models/sponsor_model_dataset_variable.py @@ -1,6 +1,6 @@ import datetime from dataclasses import dataclass -from typing import Self +from typing import Any, Self from clinical_mdr_api.domains.versioned_object_aggregate import ( LibraryItemAggregateRootBase, @@ -58,6 +58,7 @@ class SponsorModelDatasetVariableVO: enrich_build_order: int | None enrich_rule: str | None target_data_model_catalogue: str | None = None + extra_properties: dict[str, Any] | None = None @classmethod def from_repository_values( @@ -101,6 +102,7 @@ def from_repository_values( enrich_build_order: int | None, enrich_rule: str | None, target_data_model_catalogue: str | None = None, + extra_properties: dict[str, Any] | None = None, ) -> Self: sponsor_model_dataset_variable_vo = cls( dataset_uid=dataset_uid, @@ -142,6 +144,7 @@ def from_repository_values( enrich_build_order=enrich_build_order, enrich_rule=enrich_rule, target_data_model_catalogue=target_data_model_catalogue, + extra_properties=extra_properties or {}, ) return sponsor_model_dataset_variable_vo 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 09e8f496..dd4ebfc2 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 @@ -113,7 +113,9 @@ def get_end_day(self): # if there is one visit in last epoch we want to add a fixed 7 day period to the epoch duration # to display it in the visit overview if len(self._visits) == 1: - return self.get_start_day() + settings.fixed_week_period + return ( + self.get_start_day() if self.get_start_day() is not None else 0 + ) + settings.fixed_week_period return self.last_visit.study_day_number def get_end_week(self): @@ -134,7 +136,9 @@ def get_end_week(self): # if there is one visit in last epoch we want to add a fixed 7 day period to the epoch duration # to display it in the visit overview if len(self._visits) == 1: - return self.get_start_week() + 1 + return ( + self.get_start_week() if self.get_start_week() is not None else 0 + ) + 1 return self.last_visit.study_week_number @property diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_arm.py b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_arm.py index 39584f9b..f076e98e 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_arm.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_arm.py @@ -18,6 +18,7 @@ class StudySelectionArmVO: study_uid: str | None name: str short_name: str + label: str code: str | None description: str | None randomization_group: str | None @@ -40,6 +41,7 @@ def from_input_values( study_uid: str | None = None, name: str | None = None, short_name: str | None = None, + label: str | None = None, code: str | None = None, description: str | None = None, randomization_group: str | None = None, @@ -59,6 +61,7 @@ def from_input_values( :param study_selection_uid :param name :param short_name + :param label :param code :param description :param randomization_group @@ -86,6 +89,7 @@ def from_input_values( study_selection_uid=normalize_string(study_selection_uid), name=name or "", short_name=short_name or "", + label=label or "", code=code, description=description, randomization_group=randomization_group, 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 756efd16..016c30c7 100644 --- a/clinical-mdr-api/clinical_mdr_api/listings/query_service.py +++ b/clinical-mdr-api/clinical_mdr_api/listings/query_service.py @@ -11,6 +11,7 @@ CypherQueryBuilder, FilterDict, FilterOperator, + calculate_total_count_from_query_result, ) from common.config import settings @@ -156,13 +157,14 @@ def get_topic_codes( res = (result_array, attributes_names) result = utils.db_result_to_list(res) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(result), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return GenericFilteringReturn(items=result, total=total) @@ -210,13 +212,14 @@ def get_cdisc_ct_ver( res = (result_array, attributes_names) result = utils.db_result_to_list(res) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(result), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return GenericFilteringReturn(items=result, total=total) @@ -263,13 +266,14 @@ def get_cdisc_ct_pkg( res = (result_array, attributes_names) result = utils.db_result_to_list(res) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(result), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return GenericFilteringReturn(items=result, total=total) @@ -334,13 +338,14 @@ def get_cdisc_ct_list( res = (result_array, attributes_names) result = utils.db_result_to_list(res) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(result), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return GenericFilteringReturn(items=result, total=total) @@ -403,13 +408,14 @@ def get_cdisc_ct_val( res = (result_array, attributes_names) result = utils.db_result_to_list(res) - total = 0 - if total_count: + total = calculate_total_count_from_query_result( + len(result), page_number, page_size, total_count + ) + if total is None: count_result, _ = db.cypher_query( query=query.count_query, params=query.parameters ) - if len(count_result) > 0: - total = count_result[0][0] + total = count_result[0][0] if len(count_result) > 0 else 0 return GenericFilteringReturn(items=result, total=total) @@ -826,12 +832,14 @@ def get_ts( WITH sr, sv MATCH (sv)-->(sf:StudyField) OPTIONAL MATCH (sf)-->(:CTTermContext)-[:HAS_SELECTED_TERM]->(ctr:CTTermRoot)--> - (ctar:CTTermAttributesRoot)-[:LATEST_FINAL]->(ctav:CTTermAttributesValue)<--(:CTPackageTerm)<--(:CTPackageCodelist)<--(ctp:CTPackage) + (ctar:CTTermAttributesRoot)-[:LATEST_FINAL]->(ctav:CTTermAttributesValue) + OPTIONAL MATCH (ctav)<--(:CTPackageTerm)<--(:CTPackageCodelist)<--(ctp:CTPackage) + OPTIONAL MATCH (ctr)-->(ctnr:CTTermNameRoot)-[:LATEST_FINAL]->(ctnv:CTTermNameValue) OPTIONAL MATCH (sf)-->(dtr:DictionaryTermRoot)-->(dtv:DictionaryTermValue) OPTIONAL MATCH (sf)-[:HAS_REASON_FOR_NULL_VALUE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(ct_null:CTTermRoot{{uid:'{settings.ct_uid_na_value}'}}) OPTIONAL MATCH (sf)-[:HAS_REASON_FOR_NULL_VALUE]->(pinf_ctx:CTTermContext)-[:HAS_SELECTED_TERM]->(ct_pinf:CTTermRoot) WHERE (pinf_ctx)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]-> - (:CTCodelistTerm {{submission_value:'{settings.ct_submval_positive_infinity}'}})-[:HAS_TERM_ROOT]->(ctr) + (:CTCodelistTerm {{submission_value:'{settings.ct_submval_positive_infinity}'}})-[:HAS_TERM_ROOT]->(ctr) WITH *, CASE sf.field_name WHEN 'disease_condition_or_indication_codes' THEN 'C112038' @@ -915,6 +923,7 @@ def get_ts( nclt.submission_value AS TSPARM, controlled_by AS controlled_by, CASE + WHEN ctnv IS NOT NULL THEN ctnv.name WHEN controlled_by = 'CDISC' AND ct_null IS NULL THEN cclt.submission_value WHEN controlled_by = 'Dictionary' THEN dtv.name WHEN sf.value = [] THEN NULL @@ -926,7 +935,7 @@ def get_ts( ELSE '' END AS TSVALNF, CASE - WHEN controlled_by = 'CDISC' AND ct_null IS NULL THEN ctr.concept_id + WHEN controlled_by = 'CDISC' AND ct_null IS NULL THEN ctav.concept_id WHEN controlled_by = 'Dictionary' THEN dtv.dictionary_id ELSE '' END AS TSVALCD, diff --git a/clinical-mdr-api/clinical_mdr_api/main.py b/clinical-mdr-api/clinical_mdr_api/main.py index 81c78172..9b994113 100644 --- a/clinical-mdr-api/clinical_mdr_api/main.py +++ b/clinical-mdr-api/clinical_mdr_api/main.py @@ -259,6 +259,7 @@ async def value_error_handler(request: Request, exception: ValueError): prefix="/notifications", tags=["Notifications"], ) +app.include_router(routers.iso_router, prefix="/iso", tags=["ISO Standards"]) app.include_router( routers.odm_study_events_router, prefix="/concepts/odms/study-events", 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 82b6d52a..9c2c7690 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 @@ -414,6 +414,10 @@ class ActivityItemClassMappingInput(PatchInputModel): variable_class_uids: list[str] = Field(default_factory=list) +class ValidCodelistMappingInput(PatchInputModel): + valid_codelist_uids: list[str] = Field(default_factory=list) + + class ActivityItemClassVersion(ActivityItemClass): """ Class for storing ActivityItemClass and calculation of differences 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 678a2acb..2bcea67d 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 @@ -156,6 +156,7 @@ def from_activity_ar( ct_terms=ct_terms, unit_definitions=unit_definitions, is_adam_param_specific=activity_item.is_adam_param_specific, + text_value=activity_item.text_value, ) ) @@ -269,6 +270,7 @@ def from_activity_instance_ar_objects( ct_terms=ct_terms, unit_definitions=unit_definitions, is_adam_param_specific=activity_item.is_adam_param_specific, + text_value=activity_item.text_value, ) ) @@ -451,6 +453,7 @@ class SimplifiedActivityItem(BaseModel): unit_definitions: list[CompactUnitDefinition] = Field(default_factory=list) activity_item_class: Annotated[SimpleActivityItemClassForActivityInstance, Field()] is_adam_param_specific: Annotated[bool, Field()] + text_value: Annotated[str | None, Field()] = None class SimpleActivityInstanceGrouping(SimpleActivityGrouping): @@ -516,6 +519,7 @@ def from_repository_input(cls, overview: dict[str, Any]): is_adam_param_specific=activity_item.get( "is_adam_param_specific", False ), + text_value=activity_item.get("text_value"), ) ) 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 fca797d4..4498686f 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 @@ -99,6 +99,7 @@ class ActivityItem(BaseModel): ct_terms: list[CompactCTTerm] = Field(default_factory=list) unit_definitions: list[CompactUnitDefinition] = Field(default_factory=list) is_adam_param_specific: Annotated[bool, Field()] + text_value: Annotated[str | None, Field()] = None class ActivityItemCreateInput(PostInputModel): @@ -110,3 +111,4 @@ class CTTermsInput(PostInputModel): ct_terms: Annotated[list[CTTermsInput], Field()] unit_definition_uids: Annotated[list[str], Field()] is_adam_param_specific: Annotated[bool, Field()] + text_value: Annotated[str | None, Field()] = None diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_common_models.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_common_models.py index 595d5c7d..9d94276e 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_common_models.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_common_models.py @@ -4,6 +4,7 @@ from clinical_mdr_api.domains.concepts.concept_base import ConceptARBase from clinical_mdr_api.domains.concepts.odms.vendor_attribute import OdmVendorAttributeAR +from clinical_mdr_api.domains.enums import OdmTranslatedTextTypeEnum from clinical_mdr_api.models.utils import BaseModel, PostInputModel from clinical_mdr_api.models.validators import is_language_supported @@ -282,20 +283,12 @@ class OdmAliasModel(BaseModel, frozen=True): # type: ignore[misc] context: Annotated[str, Field(min_length=1)] -class OdmDescriptionModel(BaseModel, frozen=True): # type: ignore[misc] - name: Annotated[str, Field(min_length=1)] +class OdmTranslatedTextModel(BaseModel, frozen=True): # type: ignore[misc] + text_type: Annotated[OdmTranslatedTextTypeEnum, Field()] language: Annotated[ str, StringConstraints(to_lower=True, strip_whitespace=True, min_length=1) ] - description: Annotated[ - str | None, Field(json_schema_extra={"nullable": True, "format": "html"}) - ] = None - instruction: Annotated[ - str | None, Field(json_schema_extra={"nullable": True, "format": "html"}) - ] = None - sponsor_instruction: Annotated[ - str | None, Field(json_schema_extra={"nullable": True, "format": "html"}) - ] = None + text: Annotated[str, Field(json_schema_extra={"format": "html"})] _language_validator = field_validator("language")(is_language_supported) diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_condition.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_condition.py index 606438b6..fe1f1aa5 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_condition.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_condition.py @@ -11,16 +11,19 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmFormalExpressionModel, + OdmTranslatedTextModel, +) +from clinical_mdr_api.models.validators import ( + has_english_description, + translated_text_uniqueness_check, ) -from clinical_mdr_api.models.validators import has_english_description class OdmCondition(ConceptModel): oid: Annotated[str | None, Field()] formal_expressions: Annotated[list[OdmFormalExpressionModel], Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] possible_actions: Annotated[list[str], Field()] @@ -41,8 +44,9 @@ def from_odm_condition_ar(cls, odm_condition_ar: OdmConditionAR) -> Self: odm_condition_ar.concept_vo.formal_expressions, key=lambda item: item.context, ), - descriptions=sorted( - odm_condition_ar.concept_vo.descriptions, key=lambda item: item.name + translated_texts=sorted( + odm_condition_ar.concept_vo.translated_texts, + key=lambda item: (item.text_type.value, item.text), ), aliases=sorted( odm_condition_ar.concept_vo.aliases, key=lambda item: item.name @@ -56,10 +60,13 @@ def from_odm_condition_ar(cls, odm_condition_ar: OdmConditionAR) -> Self: class OdmConditionPostInput(ConceptPostInput): oid: Annotated[str | None, Field(min_length=1)] = None formal_expressions: Annotated[list[OdmFormalExpressionModel], Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) @@ -68,10 +75,13 @@ class OdmConditionPatchInput(ConceptPatchInput): name: Annotated[str, Field(min_length=1)] oid: Annotated[str | None, Field(min_length=1)] formal_expressions: Annotated[list[OdmFormalExpressionModel], Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_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 e8ae0105..cce821de 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 @@ -21,8 +21,10 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmRefVendorPostInput, + OdmTranslatedTextModel, + OdmVendorElementRelationPostInput, + OdmVendorRelationPostInput, ) from clinical_mdr_api.models.concepts.odms.odm_item_group import OdmItemGroupRefModel from clinical_mdr_api.models.concepts.odms.odm_vendor_attribute import ( @@ -33,7 +35,10 @@ OdmVendorElementRelationModel, ) from clinical_mdr_api.models.utils import BaseModel, PostInputModel -from clinical_mdr_api.models.validators import has_english_description +from clinical_mdr_api.models.validators import ( + has_english_description, + translated_text_uniqueness_check, +) from common.config import settings from common.utils import booltostr @@ -44,7 +49,7 @@ class OdmForm(ConceptModel): sdtm_version: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None ) - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] item_groups: Annotated[list[OdmItemGroupRefModel], Field()] vendor_elements: Annotated[list[OdmVendorElementRelationModel], Field()] @@ -83,8 +88,9 @@ def from_odm_form_ar( version=odm_form_ar.item_metadata.version, change_description=odm_form_ar.item_metadata.change_description, author_username=odm_form_ar.item_metadata.author_username, - descriptions=sorted( - odm_form_ar.concept_vo.descriptions, key=lambda item: item.name + translated_texts=sorted( + odm_form_ar.concept_vo.translated_texts, + key=lambda item: (item.text_type.value, item.text), ), aliases=sorted(odm_form_ar.concept_vo.aliases, key=lambda item: item.name), item_groups=sorted( @@ -188,6 +194,7 @@ def from_odm_form_uid( if odm_form_ref_vo is not None: odm_form_ref_model = cls( uid=uid, + oid=odm_form_ref_vo.oid, name=odm_form_ref_vo.name, version=odm_form_ref_vo.version, order_number=odm_form_ref_vo.order_number, @@ -198,6 +205,7 @@ def from_odm_form_uid( else: odm_form_ref_model = cls( uid=uid, + oid=None, name=None, version=None, order_number=None, @@ -208,6 +216,7 @@ def from_odm_form_uid( return odm_form_ref_model uid: Annotated[str, Field()] + 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, Field(json_schema_extra={"nullable": True})] = 999999 @@ -222,10 +231,20 @@ class OdmFormPostInput(ConceptPostInput): oid: Annotated[str | None, Field(min_length=1)] = None sdtm_version: Annotated[str | None, Field()] = None repeating: Annotated[str, Field(min_length=1)] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: list[OdmAliasModel] = Field(default_factory=list) + vendor_elements: list[OdmVendorElementRelationPostInput] = Field( + default_factory=list + ) + vendor_element_attributes: list[OdmVendorRelationPostInput] = Field( + default_factory=list + ) + vendor_attributes: list[OdmVendorRelationPostInput] = Field(default_factory=list) - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) @@ -235,10 +254,16 @@ class OdmFormPatchInput(ConceptPatchInput): oid: Annotated[str | None, Field(min_length=1)] sdtm_version: Annotated[str | None, Field()] repeating: Annotated[str | None, Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: list[OdmAliasModel] = Field(default_factory=list) + vendor_elements: Annotated[list[OdmVendorElementRelationPostInput], Field()] + vendor_element_attributes: Annotated[list[OdmVendorRelationPostInput], Field()] + vendor_attributes: Annotated[list[OdmVendorRelationPostInput], Field()] - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) 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 3ba851ca..51c69771 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 @@ -34,9 +34,11 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmRefVendor, OdmRefVendorAttributeModel, + OdmTranslatedTextModel, + OdmVendorElementRelationPostInput, + OdmVendorRelationPostInput, ) from clinical_mdr_api.models.concepts.odms.odm_vendor_attribute import ( OdmVendorAttributeRelationModel, @@ -52,8 +54,11 @@ SimpleDictionaryTermModel, SimpleTermModel, ) -from clinical_mdr_api.models.utils import BaseModel, InputModel, PostInputModel -from clinical_mdr_api.models.validators import has_english_description +from clinical_mdr_api.models.utils import BaseModel, InputModel +from clinical_mdr_api.models.validators import ( + has_english_description, + translated_text_uniqueness_check, +) from common.config import settings @@ -201,6 +206,7 @@ def from_unit_definition_uid( simple_unit_definition_model = cls( uid=unit_definition_uid, name=unit_definition.concept_vo.name, + version=unit_definition.item_metadata.version, mandatory=unit_definition_rel.mandatory, order=unit_definition_rel.order, ucum=ucum, @@ -210,6 +216,7 @@ def from_unit_definition_uid( simple_unit_definition_model = cls( uid=unit_definition_uid, name=None, + version=None, mandatory=False, order=None, ucum=None, @@ -219,6 +226,7 @@ def from_unit_definition_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 mandatory: Annotated[bool, Field()] = False order: Annotated[int | None, Field(json_schema_extra={"nullable": True})] = None ucum: Annotated[ @@ -244,7 +252,7 @@ class OdmItem(ConceptModel): ) origin: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None comment: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] unit_definitions: Annotated[list[OdmItemUnitDefinitionWithRelationship], Field()] codelist: Annotated[ @@ -252,7 +260,7 @@ class OdmItem(ConceptModel): Field(json_schema_extra={"nullable": True}), ] = None terms: Annotated[list[OdmItemTermRelationshipModel], Field()] - activity_instances: Annotated[list, Field()] + activity_instances: Annotated[list["ActivityInstanceRel"], Field()] vendor_elements: Annotated[list[OdmVendorElementRelationModel], Field()] vendor_attributes: Annotated[list[OdmVendorAttributeRelationModel], Field()] vendor_element_attributes: Annotated[ @@ -303,8 +311,9 @@ def from_odm_item_ar( version=odm_item_ar.item_metadata.version, change_description=odm_item_ar.item_metadata.change_description, author_username=odm_item_ar.item_metadata.author_username, - descriptions=sorted( - odm_item_ar.concept_vo.descriptions, key=lambda item: item.name + translated_texts=sorted( + odm_item_ar.concept_vo.translated_texts, + key=lambda item: (item.text_type.value, item.text), ), aliases=sorted(odm_item_ar.concept_vo.aliases, key=lambda item: item.name), unit_definitions=sorted( @@ -322,8 +331,11 @@ def from_odm_item_ar( key=lambda item: item.uid, ), codelist=CTCodelistAttributesSimpleModel.from_codelist_uid( - uid=odm_item_ar.concept_vo.codelist_uid, + uid=getattr(odm_item_ar.concept_vo.codelist, "uid", None), find_codelist_attribute_by_codelist_uid=find_codelist_attribute_by_codelist_uid, + allows_multi_choice=getattr( + odm_item_ar.concept_vo.codelist, "allows_multi_choice", None + ), ), terms=sorted( [ @@ -339,6 +351,15 @@ def from_odm_item_ar( activity_instances=[ ActivityInstanceRel( activity_instance_uid=activity_instance["activity_instance_uid"], + activity_instance_name=activity_instance.get( + "activity_instance_name", None + ), + activity_instance_adam_param_code=activity_instance.get( + "activity_instance_adam_param_code", None + ), + activity_instance_topic_code=activity_instance.get( + "activity_instance_topic_code", None + ), activity_item_class_uid=activity_instance[ "activity_item_class_uid" ], @@ -516,7 +537,7 @@ def from_odm_item_uid( class OdmItemTermRelationshipInput(InputModel): uid: Annotated[str, Field(min_length=1)] mandatory: Annotated[bool, Field()] = True - order: Annotated[int | None, Field()] = 999999 + order: Annotated[int | None, Field()] = None display_text: Annotated[str | None, Field()] = None @@ -554,6 +575,11 @@ def check_length_and_significant_digits(model): return model +class OdmItemCodelist(BaseModel): + uid: Annotated[str, Field(min_length=1)] + allows_multi_choice: Annotated[bool, Field()] = False + + class OdmItemPostInput(ConceptPostInput): oid: Annotated[str | None, Field(min_length=1)] = None datatype: Annotated[str, Field(min_length=1)] @@ -566,21 +592,31 @@ class OdmItemPostInput(ConceptPostInput): sds_var_name: Annotated[str | None, Field()] = None origin: Annotated[str | None, Field()] = None comment: Annotated[str | None, Field()] = None - descriptions: list[OdmDescriptionModel] = Field(default_factory=list) + translated_texts: list[OdmTranslatedTextModel] = Field(default_factory=list) aliases: list[OdmAliasModel] = Field(default_factory=list) - codelist_uid: Annotated[str | None, Field(min_length=1)] = None + codelist: Annotated[OdmItemCodelist | None, Field()] = None unit_definitions: list[OdmItemUnitDefinitionRelationshipInput] = Field( default_factory=list ) terms: list[OdmItemTermRelationshipInput] = Field(default_factory=list) + vendor_elements: list[OdmVendorElementRelationPostInput] = Field( + default_factory=list + ) + vendor_element_attributes: list[OdmVendorRelationPostInput] = Field( + default_factory=list + ) + vendor_attributes: list[OdmVendorRelationPostInput] = Field(default_factory=list) _ = model_validator(mode="after")(check_length_and_significant_digits) - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) -class ActivityInstanceRel(PostInputModel): +class ActivityInstanceRelInput(InputModel): 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)] @@ -592,6 +628,12 @@ class ActivityInstanceRel(PostInputModel): value_dependent_map: Annotated[str | None, Field()] = None +class ActivityInstanceRel(ActivityInstanceRelInput): + activity_instance_name: Annotated[str | None, Field()] + activity_instance_adam_param_code: Annotated[str | None, Field()] + activity_instance_topic_code: Annotated[str | None, Field()] + + class OdmItemPatchInput(ConceptPatchInput): name: Annotated[str, Field(min_length=1)] oid: Annotated[str | None, Field(min_length=1)] @@ -603,17 +645,23 @@ class OdmItemPatchInput(ConceptPatchInput): sds_var_name: Annotated[str | None, Field()] origin: Annotated[str | None, Field()] comment: Annotated[str | None, Field()] - descriptions: list[OdmDescriptionModel] = Field(default_factory=list) + translated_texts: list[OdmTranslatedTextModel] = Field(default_factory=list) aliases: list[OdmAliasModel] = Field(default_factory=list) unit_definitions: list[OdmItemUnitDefinitionRelationshipInput] = Field( default_factory=list ) - codelist_uid: Annotated[str | None, Field(min_length=1)] + codelist: Annotated[OdmItemCodelist | None, Field()] terms: Annotated[list[OdmItemTermRelationshipInput], Field()] - activity_instances: list[ActivityInstanceRel] = Field(default_factory=list) + vendor_elements: Annotated[list[OdmVendorElementRelationPostInput], Field()] + vendor_element_attributes: Annotated[list[OdmVendorRelationPostInput], Field()] + vendor_attributes: Annotated[list[OdmVendorRelationPostInput], Field()] + activity_instances: list[ActivityInstanceRelInput] = Field(default_factory=list) _ = model_validator(mode="after")(check_length_and_significant_digits) - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) 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 dd330da4..538c8397 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 @@ -27,10 +27,12 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmRefVendor, OdmRefVendorAttributeModel, OdmRefVendorPostInput, + OdmTranslatedTextModel, + OdmVendorElementRelationPostInput, + OdmVendorRelationPostInput, ) from clinical_mdr_api.models.concepts.odms.odm_item import OdmItemRefModel from clinical_mdr_api.models.concepts.odms.odm_vendor_attribute import ( @@ -46,6 +48,7 @@ from clinical_mdr_api.models.utils import BaseModel, PostInputModel from clinical_mdr_api.models.validators import ( has_english_description, + translated_text_uniqueness_check, validate_string_represents_boolean, ) from common.config import settings @@ -64,7 +67,7 @@ class OdmItemGroup(ConceptModel): origin: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None purpose: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None comment: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] sdtm_domains: Annotated[list[SimpleCodelistTermModel], Field()] items: Annotated[list[OdmItemRefModel], Field()] @@ -123,8 +126,9 @@ def from_odm_item_group_ar( version=odm_item_group_ar.item_metadata.version, change_description=odm_item_group_ar.item_metadata.change_description, author_username=odm_item_group_ar.item_metadata.author_username, - descriptions=sorted( - odm_item_group_ar.concept_vo.descriptions, key=lambda item: item.name + translated_texts=sorted( + odm_item_group_ar.concept_vo.translated_texts, + key=lambda item: (item.text_type.value, item.text), ), aliases=sorted( odm_item_group_ar.concept_vo.aliases, key=lambda item: item.name @@ -293,14 +297,24 @@ class OdmItemGroupPostInput(ConceptPostInput): origin: Annotated[str | None, Field()] = None purpose: Annotated[str | None, Field()] = None comment: Annotated[str | None, Field()] = None - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: list[OdmAliasModel] = Field(default_factory=list) sdtm_domain_uids: Annotated[list[str], Field()] + vendor_elements: list[OdmVendorElementRelationPostInput] = Field( + default_factory=list + ) + vendor_element_attributes: list[OdmVendorRelationPostInput] = Field( + default_factory=list + ) + vendor_attributes: list[OdmVendorRelationPostInput] = Field(default_factory=list) _validate_string_represents_boolean = field_validator( "repeating", "is_reference_data", mode="before" )(validate_string_represents_boolean) - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) @@ -314,14 +328,20 @@ class OdmItemGroupPatchInput(ConceptPatchInput): origin: Annotated[str | None, Field()] purpose: Annotated[str | None, Field()] comment: Annotated[str | None, Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: list[OdmAliasModel] = Field(default_factory=list) sdtm_domain_uids: Annotated[list[str], Field()] + vendor_elements: Annotated[list[OdmVendorElementRelationPostInput], Field()] + vendor_element_attributes: Annotated[list[OdmVendorRelationPostInput], Field()] + vendor_attributes: Annotated[list[OdmVendorRelationPostInput], Field()] _validate_string_represents_boolean = field_validator( "repeating", "is_reference_data", mode="before" )(validate_string_represents_boolean) - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_method.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_method.py index e194fb9f..94f46ed4 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_method.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_method.py @@ -11,17 +11,20 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmFormalExpressionModel, + OdmTranslatedTextModel, +) +from clinical_mdr_api.models.validators import ( + has_english_description, + translated_text_uniqueness_check, ) -from clinical_mdr_api.models.validators import has_english_description class OdmMethod(ConceptModel): oid: Annotated[str | None, Field()] method_type: Annotated[str | None, Field()] formal_expressions: Annotated[list[OdmFormalExpressionModel], Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] possible_actions: Annotated[list[str], Field()] @@ -43,8 +46,9 @@ def from_odm_method_ar(cls, odm_method_ar: OdmMethodAR) -> Self: odm_method_ar.concept_vo.formal_expressions, key=lambda item: item.context, ), - descriptions=sorted( - odm_method_ar.concept_vo.descriptions, key=lambda item: item.name + translated_texts=sorted( + odm_method_ar.concept_vo.translated_texts, + key=lambda item: (item.text_type.value, item.text), ), aliases=sorted( odm_method_ar.concept_vo.aliases, key=lambda item: item.name @@ -59,10 +63,13 @@ class OdmMethodPostInput(ConceptPostInput): oid: Annotated[str | None, Field(min_length=1)] = None method_type: Annotated[str | None, Field(min_length=1)] = None formal_expressions: Annotated[list[OdmFormalExpressionModel], Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] = Field(default_factory=list) - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) @@ -72,10 +79,13 @@ class OdmMethodPatchInput(ConceptPatchInput): oid: Annotated[str | None, Field(min_length=1)] method_type: Annotated[str | None, Field(min_length=1)] formal_expressions: Annotated[list[OdmFormalExpressionModel], Field()] - descriptions: Annotated[list[OdmDescriptionModel], Field()] + translated_texts: Annotated[list[OdmTranslatedTextModel], Field()] aliases: Annotated[list[OdmAliasModel], Field()] = Field(default_factory=list) - _english_description_validator = field_validator("descriptions")( + _translated_text_uniqueness_check = field_validator("translated_texts")( + translated_text_uniqueness_check + ) + _english_description_validator = field_validator("translated_texts")( has_english_description ) diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_attribute.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_attribute.py index 4de234aa..f2693a43 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_attribute.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_attribute.py @@ -25,6 +25,7 @@ ) from clinical_mdr_api.models.utils import BaseModel from clinical_mdr_api.models.validators import ( + validate_first_character_is_lowercase, validate_name_only_contains_letters, validate_regex, ) @@ -256,6 +257,9 @@ class OdmVendorAttributePostInput(ConceptPostInput): _validate_name_only_contains_letters = field_validator("name", mode="before")( validate_name_only_contains_letters ) + _validate_first_character_is_lowercase = field_validator("name", mode="after")( + validate_first_character_is_lowercase + ) @model_validator(mode="before") @classmethod @@ -281,6 +285,12 @@ class OdmVendorAttributePatchInput(ConceptPatchInput): value_regex: Annotated[str | None, Field(min_length=1)] _validate_regex = field_validator("value_regex", mode="before")(validate_regex) + _validate_name_only_contains_letters = field_validator("name", mode="before")( + validate_name_only_contains_letters + ) + _validate_first_character_is_lowercase = field_validator("name", mode="after")( + validate_first_character_is_lowercase + ) class OdmVendorAttributeVersion(OdmVendorAttribute): diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_element.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_element.py index 4f8dc91d..6019e9a5 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_element.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_vendor_element.py @@ -23,7 +23,10 @@ OdmVendorNamespaceSimpleModel, ) from clinical_mdr_api.models.utils import BaseModel -from clinical_mdr_api.models.validators import validate_name_only_contains_letters +from clinical_mdr_api.models.validators import ( + validate_first_character_is_uppercase, + validate_name_only_contains_letters, +) class OdmVendorElement(ConceptModel): @@ -60,7 +63,9 @@ def from_odm_vendor_element_ar( uid=vendor_attribute_uid, find_odm_vendor_attribute_by_uid=find_odm_vendor_attribute_by_uid, ) - for vendor_attribute_uid in odm_vendor_element_ar.concept_vo.vendor_attribute_uids + for vendor_attribute_uid in set( + odm_vendor_element_ar.concept_vo.vendor_attribute_uids + ) ], key=lambda item: item.name or "", ), @@ -138,12 +143,22 @@ class OdmVendorElementPostInput(ConceptPostInput): _validate_name_only_contains_letters = field_validator("name", mode="before")( validate_name_only_contains_letters ) + _validate_first_character_is_uppercase = field_validator("name", mode="after")( + validate_first_character_is_uppercase + ) class OdmVendorElementPatchInput(ConceptPatchInput): name: Annotated[str, Field(min_length=1)] compatible_types: Annotated[list[VendorElementCompatibleType], Field(min_length=1)] + _validate_name_only_contains_letters = field_validator("name", mode="before")( + validate_name_only_contains_letters + ) + _validate_first_character_is_uppercase = field_validator("name", mode="after")( + validate_first_character_is_uppercase + ) + class OdmVendorElementVersion(OdmVendorElement): """ diff --git a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist.py b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist.py index e7d3fa12..4e5bb3b4 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist.py +++ b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist.py @@ -48,7 +48,7 @@ def from_ct_codelist_ar( nci_preferred_name=ct_codelist_attributes_ar.ct_codelist_vo.preferred_term, definition=ct_codelist_attributes_ar.ct_codelist_vo.definition, extensible=ct_codelist_attributes_ar.ct_codelist_vo.extensible, - ordinal=ct_codelist_attributes_ar.ct_codelist_vo.ordinal, + is_ordinal=ct_codelist_attributes_ar.ct_codelist_vo.is_ordinal, library_name=Library.from_library_vo( ct_codelist_attributes_ar.library ).name, @@ -91,7 +91,7 @@ def from_ct_codelist_ar( extensible: Annotated[bool, Field()] - ordinal: Annotated[bool, Field()] + is_ordinal: Annotated[bool, Field()] sponsor_preferred_name: Annotated[str, Field()] @@ -113,6 +113,7 @@ class CTCodelistTermInput(PostInputModel): int | None, Field(gt=0, lt=settings.max_int_neo4j), ] = 999999 + ordinal: Annotated[float | None, Field()] = None submission_value: Annotated[str, Field(min_length=1)] @@ -123,7 +124,7 @@ class CTCodelistCreateInput(PostInputModel): nci_preferred_name: Annotated[str | None, Field(min_length=1)] = None definition: Annotated[str, Field(min_length=1)] extensible: Annotated[bool, Field()] - ordinal: Annotated[bool, Field()] + is_ordinal: Annotated[bool, Field()] sponsor_preferred_name: Annotated[str, Field(min_length=1)] template_parameter: Annotated[bool, Field()] parent_codelist_uid: Annotated[str | None, Field(min_length=1)] = None @@ -268,6 +269,7 @@ def from_ct_codelist_term_ar( term_uid=ct_codelist_term_ar.ct_codelist_term_vo.term_uid, submission_value=ct_codelist_term_ar.ct_codelist_term_vo.submission_value, order=ct_codelist_term_ar.ct_codelist_term_vo.order, + ordinal=ct_codelist_term_ar.ct_codelist_term_vo.ordinal, library_name=ct_codelist_term_ar.ct_codelist_term_vo.library_name, sponsor_preferred_name=ct_codelist_term_ar.ct_codelist_term_vo.sponsor_preferred_name, sponsor_preferred_name_sentence_case=ct_codelist_term_ar.ct_codelist_term_vo.sponsor_preferred_name_sentence_case, @@ -290,6 +292,7 @@ def from_ct_codelist_term_ar( int | None, Field(json_schema_extra={"nullable": True}), ] + ordinal: Annotated[float | None, Field(json_schema_extra={"nullable": True})] = None library_name: Annotated[str | None, Field()] sponsor_preferred_name: Annotated[str, Field()] diff --git a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist_attributes.py b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist_attributes.py index 014183e8..d980b16c 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_codelist_attributes.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Annotated, Callable, Self, overload -from pydantic import Field +from pydantic import ConfigDict, Field from clinical_mdr_api.descriptions.general import CHANGES_FIELD_DESC from clinical_mdr_api.domains.controlled_terminologies.ct_codelist_attributes import ( @@ -24,7 +24,7 @@ def from_ct_codelist_ar(cls, ct_codelist_ar: CTCodelistAttributesAR) -> Self: nci_preferred_name=ct_codelist_ar.ct_codelist_vo.preferred_term, definition=ct_codelist_ar.ct_codelist_vo.definition, extensible=ct_codelist_ar.ct_codelist_vo.extensible, - ordinal=ct_codelist_ar.ct_codelist_vo.ordinal, + is_ordinal=ct_codelist_ar.ct_codelist_vo.is_ordinal, library_name=Library.from_library_vo(ct_codelist_ar.library).name, start_date=ct_codelist_ar.item_metadata.start_date, end_date=ct_codelist_ar.item_metadata.end_date, @@ -47,7 +47,7 @@ def from_ct_codelist_ar_without_common_codelist_fields( nci_preferred_name=ct_codelist_ar.ct_codelist_vo.preferred_term, definition=ct_codelist_ar.ct_codelist_vo.definition, extensible=ct_codelist_ar.ct_codelist_vo.extensible, - ordinal=ct_codelist_ar.ct_codelist_vo.ordinal, + is_ordinal=ct_codelist_ar.ct_codelist_vo.is_ordinal, start_date=ct_codelist_ar.item_metadata.start_date, end_date=ct_codelist_ar.item_metadata.end_date, status=ct_codelist_ar.item_metadata.status.value, @@ -88,7 +88,7 @@ def from_ct_codelist_ar_without_common_codelist_fields( extensible: Annotated[bool, Field()] - ordinal: Annotated[bool, Field()] + is_ordinal: Annotated[bool, Field()] library_name: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None @@ -117,6 +117,8 @@ def from_ct_codelist_ar_without_common_codelist_fields( class CTCodelistAttributesSimpleModel(BaseModel): + model_config = ConfigDict(extra="allow") + @overload @classmethod def from_codelist_uid( @@ -125,6 +127,7 @@ def from_codelist_uid( find_codelist_attribute_by_codelist_uid: Callable[ [str], CTCodelistAttributesAR | None ], + **kwargs, ) -> Self: ... @overload @classmethod @@ -134,6 +137,7 @@ def from_codelist_uid( find_codelist_attribute_by_codelist_uid: Callable[ [str], CTCodelistAttributesAR | None ], + **kwargs, ) -> None: ... @classmethod def from_codelist_uid( @@ -142,30 +146,35 @@ def from_codelist_uid( find_codelist_attribute_by_codelist_uid: Callable[ [str], CTCodelistAttributesAR | None ], + **kwargs, ) -> Self | None: - simple_codelist_attribute_model = None - if uid is not None: codelist_attribute = find_codelist_attribute_by_codelist_uid(uid) if codelist_attribute is not None: - simple_codelist_attribute_model = cls( + return cls( uid=uid, name=codelist_attribute._ct_codelist_attributes_vo.name, + version=codelist_attribute.item_metadata.version, submission_value=codelist_attribute._ct_codelist_attributes_vo.submission_value, preferred_term=codelist_attribute._ct_codelist_attributes_vo.preferred_term, + **kwargs, ) - else: - simple_codelist_attribute_model = cls( - uid=uid, - name=None, - submission_value=None, - preferred_term=None, - ) - return simple_codelist_attribute_model + + return cls( + uid=uid, + name=None, + version=None, + submission_value=None, + preferred_term=None, + **kwargs, + ) + + return None 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 submission_value: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None @@ -188,5 +197,5 @@ class CTCodelistAttributesEditInput(PatchInputModel): nci_preferred_name: Annotated[str | None, Field(min_length=1)] = None definition: Annotated[str | None, Field(min_length=1)] = None extensible: Annotated[bool | None, Field()] = None - ordinal: Annotated[bool | None, Field()] = None + is_ordinal: Annotated[bool | None, Field()] = None change_description: Annotated[str, Field(min_length=1)] 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 6d620d20..2481004e 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 @@ -119,6 +119,7 @@ class CTTermCodelistInput(BaseModel): codelist_uid: Annotated[str, Field()] submission_value: Annotated[str, Field()] order: Annotated[int | None, Field()] = None + ordinal: Annotated[float | None, Field()] = None class CTTermCreateInput(PostInputModel): diff --git a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term_codelist.py b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term_codelist.py index dc3bd07b..dd1c09f4 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term_codelist.py +++ b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term_codelist.py @@ -15,6 +15,7 @@ class CTTermCodelist(BaseModel): ] submission_value: Annotated[str, Field()] order: Annotated[int | None, Field(json_schema_extra={"nullable": True})] + ordinal: Annotated[float | None, Field(json_schema_extra={"nullable": True})] = None start_date: datetime library_name: Annotated[str, Field()] diff --git a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset.py b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset.py index f2350615..04390d2c 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset.py +++ b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset.py @@ -1,4 +1,4 @@ -from typing import Annotated, Self +from typing import Annotated, Any, Self from pydantic import ConfigDict, Field @@ -11,7 +11,7 @@ class SponsorModelDataset(SponsorModelBase): - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True, extra="allow") uid: Annotated[ str | None, @@ -216,34 +216,46 @@ def from_sponsor_model_dataset_ar( cls, sponsor_model_dataset_ar: SponsorModelDatasetAR, ) -> Self: - return cls( - uid=sponsor_model_dataset_ar.uid, - is_basic_std=sponsor_model_dataset_ar.sponsor_model_dataset_vo.is_basic_std, - implemented_dataset_class=sponsor_model_dataset_ar.sponsor_model_dataset_vo.implemented_dataset_class, - xml_path=sponsor_model_dataset_ar.sponsor_model_dataset_vo.xml_path, - xml_title=sponsor_model_dataset_ar.sponsor_model_dataset_vo.xml_title, - structure=sponsor_model_dataset_ar.sponsor_model_dataset_vo.structure, - purpose=sponsor_model_dataset_ar.sponsor_model_dataset_vo.purpose, - keys=sponsor_model_dataset_ar.sponsor_model_dataset_vo.keys, - sort_keys=sponsor_model_dataset_ar.sponsor_model_dataset_vo.sort_keys, - is_cdisc_std=sponsor_model_dataset_ar.sponsor_model_dataset_vo.is_cdisc_std, - source_ig=sponsor_model_dataset_ar.sponsor_model_dataset_vo.source_ig, - standard_ref=sponsor_model_dataset_ar.sponsor_model_dataset_vo.standard_ref, - comment=sponsor_model_dataset_ar.sponsor_model_dataset_vo.comment, - ig_comment=sponsor_model_dataset_ar.sponsor_model_dataset_vo.ig_comment, - map_domain_flag=sponsor_model_dataset_ar.sponsor_model_dataset_vo.map_domain_flag, - suppl_qual_flag=sponsor_model_dataset_ar.sponsor_model_dataset_vo.suppl_qual_flag, - include_in_raw=sponsor_model_dataset_ar.sponsor_model_dataset_vo.include_in_raw, - gen_raw_seqno_flag=sponsor_model_dataset_ar.sponsor_model_dataset_vo.gen_raw_seqno_flag, - enrich_build_order=sponsor_model_dataset_ar.sponsor_model_dataset_vo.enrich_build_order, - label=sponsor_model_dataset_ar.sponsor_model_dataset_vo.label, - state=sponsor_model_dataset_ar.sponsor_model_dataset_vo.state, - extended_domain=sponsor_model_dataset_ar.sponsor_model_dataset_vo.extended_domain, - library_name=Library.from_library_vo(sponsor_model_dataset_ar.library).name, - ) + base_data = { + "uid": sponsor_model_dataset_ar.uid, + "is_basic_std": sponsor_model_dataset_ar.sponsor_model_dataset_vo.is_basic_std, + "implemented_dataset_class": sponsor_model_dataset_ar.sponsor_model_dataset_vo.implemented_dataset_class, + "xml_path": sponsor_model_dataset_ar.sponsor_model_dataset_vo.xml_path, + "xml_title": sponsor_model_dataset_ar.sponsor_model_dataset_vo.xml_title, + "structure": sponsor_model_dataset_ar.sponsor_model_dataset_vo.structure, + "purpose": sponsor_model_dataset_ar.sponsor_model_dataset_vo.purpose, + "keys": sponsor_model_dataset_ar.sponsor_model_dataset_vo.keys, + "sort_keys": sponsor_model_dataset_ar.sponsor_model_dataset_vo.sort_keys, + "is_cdisc_std": sponsor_model_dataset_ar.sponsor_model_dataset_vo.is_cdisc_std, + "source_ig": sponsor_model_dataset_ar.sponsor_model_dataset_vo.source_ig, + "standard_ref": sponsor_model_dataset_ar.sponsor_model_dataset_vo.standard_ref, + "comment": sponsor_model_dataset_ar.sponsor_model_dataset_vo.comment, + "ig_comment": sponsor_model_dataset_ar.sponsor_model_dataset_vo.ig_comment, + "map_domain_flag": sponsor_model_dataset_ar.sponsor_model_dataset_vo.map_domain_flag, + "suppl_qual_flag": sponsor_model_dataset_ar.sponsor_model_dataset_vo.suppl_qual_flag, + "include_in_raw": sponsor_model_dataset_ar.sponsor_model_dataset_vo.include_in_raw, + "gen_raw_seqno_flag": sponsor_model_dataset_ar.sponsor_model_dataset_vo.gen_raw_seqno_flag, + "enrich_build_order": sponsor_model_dataset_ar.sponsor_model_dataset_vo.enrich_build_order, + "label": sponsor_model_dataset_ar.sponsor_model_dataset_vo.label, + "state": sponsor_model_dataset_ar.sponsor_model_dataset_vo.state, + "extended_domain": sponsor_model_dataset_ar.sponsor_model_dataset_vo.extended_domain, + "library_name": Library.from_library_vo( + sponsor_model_dataset_ar.library + ).name, + } + + # Add extra properties if they exist + if sponsor_model_dataset_ar.sponsor_model_dataset_vo.extra_properties: + base_data.update( + sponsor_model_dataset_ar.sponsor_model_dataset_vo.extra_properties + ) + + return cls(**base_data) # type: ignore[arg-type] class SponsorModelDatasetInput(InputModel): + model_config = ConfigDict(extra="allow") # Allow extra fields + target_data_model_catalogue: Annotated[str | None, Field()] = "SDTMIG" dataset_uid: Annotated[str, Field(min_length=1)] sponsor_model_name: Annotated[ @@ -295,3 +307,10 @@ class SponsorModelDatasetInput(InputModel): library_name: Annotated[ str | None, Field(description="Defaults to CDISC", min_length=1) ] = "CDISC" + + def get_extra_fields(self) -> dict[str, Any]: + """Return fields that were passed but are not in the defined model.""" + defined_fields = set(self.model_fields.keys()) + all_fields = set(self.model_dump().keys()) + extra_fields = all_fields - defined_fields + return {field: getattr(self, field) for field in extra_fields} diff --git a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset_variable.py b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset_variable.py index 70252df1..29832300 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset_variable.py +++ b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/sponsor_model_dataset_variable.py @@ -1,4 +1,4 @@ -from typing import Annotated +from typing import Annotated, Any from pydantic import ConfigDict, Field @@ -11,7 +11,7 @@ class SponsorModelDatasetVariable(SponsorModelBase): - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True, extra="allow") uid: Annotated[ str | None, Field(json_schema_extra={"source": "uid", "nullable": True}) @@ -321,45 +321,57 @@ def from_sponsor_model_dataset_variable_ar( cls, sponsor_model_dataset_variable_ar: SponsorModelDatasetVariableAR, ) -> "SponsorModelDatasetVariable": - return cls( - uid=sponsor_model_dataset_variable_ar.uid, - is_basic_std=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.is_basic_std, - label=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.label, - order=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.order, - variable_type=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.variable_type, - length=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.length, - display_format=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.display_format, - xml_datatype=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.xml_datatype, - core=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.core, - origin=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.origin, - origin_type=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.origin_type, - origin_source=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.origin_source, - role=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.role, - term=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.term, - algorithm=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.algorithm, - qualifiers=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.qualifiers, - is_cdisc_std=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.is_cdisc_std, - comment=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.comment, - ig_comment=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.ig_comment, - class_table=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.class_table, - class_column=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.class_column, - map_var_flag=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.map_var_flag, - fixed_mapping=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.fixed_mapping, - include_in_raw=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.include_in_raw, - nn_internal=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.nn_internal, - value_lvl_where_cols=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_where_cols, - value_lvl_label_col=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_label_col, - value_lvl_collect_ct_val=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_collect_ct_val, - value_lvl_ct_codelist_id_col=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_ct_codelist_id_col, - enrich_build_order=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.enrich_build_order, - enrich_rule=sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.enrich_rule, - library_name=Library.from_library_vo( + base_data = { + "uid": sponsor_model_dataset_variable_ar.uid, + "is_basic_std": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.is_basic_std, + "label": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.label, + "order": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.order, + "variable_type": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.variable_type, + "length": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.length, + "display_format": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.display_format, + "xml_datatype": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.xml_datatype, + "core": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.core, + "origin": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.origin, + "origin_type": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.origin_type, + "origin_source": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.origin_source, + "role": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.role, + "term": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.term, + "algorithm": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.algorithm, + "qualifiers": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.qualifiers, + "is_cdisc_std": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.is_cdisc_std, + "comment": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.comment, + "ig_comment": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.ig_comment, + "class_table": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.class_table, + "class_column": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.class_column, + "map_var_flag": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.map_var_flag, + "fixed_mapping": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.fixed_mapping, + "include_in_raw": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.include_in_raw, + "nn_internal": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.nn_internal, + "value_lvl_where_cols": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_where_cols, + "value_lvl_label_col": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_label_col, + "value_lvl_collect_ct_val": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_collect_ct_val, + "value_lvl_ct_codelist_id_col": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.value_lvl_ct_codelist_id_col, + "enrich_build_order": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.enrich_build_order, + "enrich_rule": sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.enrich_rule, + "library_name": Library.from_library_vo( sponsor_model_dataset_variable_ar.library ).name, - ) + } + + # Add extra properties if they exist + if ( + sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.extra_properties + ): + base_data.update( + sponsor_model_dataset_variable_ar.sponsor_model_dataset_variable_vo.extra_properties + ) + + return cls(**base_data) # type: ignore[arg-type] class SponsorModelDatasetVariableInput(InputModel): + model_config = ConfigDict(extra="allow") # Allow extra fields + target_data_model_catalogue: Annotated[str | None, Field()] = "SDTMIG" dataset_uid: Annotated[ str, @@ -432,3 +444,10 @@ class SponsorModelDatasetVariableInput(InputModel): library_name: Annotated[ str | None, Field(description="Defaults to CDISC", min_length=1) ] = "CDISC" + + def get_extra_fields(self) -> dict[str, Any]: + """Return fields that were passed but aren't in the defined model.""" + defined_fields = set(self.model_fields.keys()) + all_fields = set(self.model_dump().keys()) + extra_fields = all_fields - defined_fields + return {field: getattr(self, field) for field in extra_fields} diff --git a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_disease_milestone.py b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_disease_milestone.py index 3b72b6cb..af4a3d01 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_disease_milestone.py +++ b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_disease_milestone.py @@ -80,7 +80,7 @@ def instantiate_study_status(cls, value): json_schema_extra={"source": "has_after.author_id", "nullable": True}, ), ] - author_username: Annotated[str | None, Field(json_schema_extra={"nullable": True})] # type: ignore[assignment] + author_username: Annotated[str | None, Field(json_schema_extra={"nullable": True, "remove_from_wildcard": True})] # type: ignore[assignment] disease_milestone_type: Annotated[ str, 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 cb18c210..039dbb35 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 @@ -225,18 +225,18 @@ json_schema_extra={"nullable": True}, ) SHOW_ACTIVITY_SUBGROUP_IN_PROTOCOL_FLOWCHART_FIELD = Field( - description="show activity subgroup in protocol flow chart", + description="show activity subgroup in protocol flowchart", json_schema_extra={"nullable": True}, ) SHOW_ACTIVITY_GROUP_IN_PROTOCOL_FLOWCHART_FIELD = Field( - description="show activity group in protocol flow chart", + description="show activity group in protocol flowchart", json_schema_extra={"nullable": True}, ) SHOW_SOA_GROUP_IN_PROTOCOL_FLOWCHART_FIELD = Field( - description="show soa group in protocol flow chart", + description="show soa group in protocol flowchart", ) SHOW_ACTIVITY_INSTANCE_IN_PROTOCOL_FLOWCHART_FIELD = Field( - description="show activity instance in operational flow chart", + description="show activity instance in operational flowchart", ) @@ -2052,7 +2052,7 @@ class StudySelectionActivityCore(StudySelection): show_activity_in_protocol_flowchart: Annotated[ bool | None, Field( - description="show activity in protocol flow chart", + description="show activity in protocol flowchart", json_schema_extra={"nullable": True}, ), ] = None @@ -2318,6 +2318,9 @@ class StudySelectionActivityCreateInput(PostInputModel): activity_subgroup_uid: Annotated[str | None, Field()] = None activity_group_uid: Annotated[str | None, Field()] = None activity_instance_uid: Annotated[str | None, Field()] = None + show_activity_in_protocol_flowchart: Annotated[ + bool, Field(description="show activity in protocol flowchart") + ] = False class StudySelectionActivityInSoACreateInput(PatchInputModel): @@ -3821,6 +3824,7 @@ class CompactStudyArm(BaseModel): uid: Annotated[str, Field(description="uid for the study arm")] name: Annotated[str, Field(description="name for the study arm")] short_name: Annotated[str, Field(description="short name for the study arm")] + label: Annotated[str, Field(description="label for the study arm")] number_of_subjects: Annotated[ int | None, Field(description="number_of_subjects for the study arm") ] = None @@ -3858,6 +3862,7 @@ def from_repository_output( uid=arm_structure["uid"], name=arm_structure["name"], short_name=arm_structure["short_name"], + label=arm_structure["label"], number_of_subjects=arm_structure["number_of_subjects"], study_cohorts=cohorts, ) @@ -3876,6 +3881,8 @@ class StudySelectionArm(StudySelection): Field(description="short name for the study arm"), ] + label: Annotated[str | None, Field(description="label for the study arm")] = None + code: Annotated[ str | None, Field( @@ -3970,6 +3977,7 @@ def from_study_selection_arm_ar_and_order( arm_uid=selection.study_selection_uid, name=selection.name, short_name=selection.short_name, + label=selection.label, code=selection.code, description=selection.description, order=order, @@ -4012,6 +4020,7 @@ def from_study_selection_history( order=study_selection_history.order, arm_uid=study_selection_history.study_selection_uid, name=study_selection_history.arm_name, + label=study_selection_history.arm_label, short_name=study_selection_history.arm_short_name, code=study_selection_history.arm_code, description=study_selection_history.arm_description, @@ -4071,6 +4080,7 @@ def from_study_selection_arm_ar__order__connected_branch_arms( arm_uid=selection.study_selection_uid, name=selection.name, short_name=selection.short_name, + label=selection.label, study_version=( study_value_version if study_value_version @@ -4107,6 +4117,8 @@ class StudySelectionArmCreateInput(PostInputModel): str | None, Field(description="short name for the study arm") ] = None + label: Annotated[str | None, Field(description="label for the study arm")] = None + code: Annotated[str | None, Field(description="code for the study arm")] = None description: Annotated[ @@ -4142,6 +4154,8 @@ class StudySelectionArmInput(PatchInputModel): str | None, Field(description="short name for the study arm") ] = None + label: Annotated[str | None, Field(description="label for the study arm")] = None + code: Annotated[str | None, Field(description="code for the study arm")] = None description: Annotated[ 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 8037d88d..8fe4d12f 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 @@ -17,6 +17,7 @@ get_latest_on_datetime_str, ) from common.config import settings +from common.utils import VisitClass, VisitSubclass class StudyVisitCreateInput(PostInputModel): @@ -58,8 +59,8 @@ class StudyVisitCreateInput(PostInputModel): end_rule: Annotated[str | None, Field()] = None visit_contact_mode_uid: Annotated[str, Field()] epoch_allocation_uid: Annotated[str | None, Field()] = None - visit_class: Annotated[str, Field()] - visit_subclass: Annotated[str | None, Field()] = None + visit_class: Annotated[VisitClass, Field()] + visit_subclass: Annotated[VisitSubclass | None, Field()] = None is_global_anchor_visit: Annotated[bool, Field()] is_soa_milestone: Annotated[bool, Field()] = False visit_name: Annotated[str | None, Field()] = None @@ -118,8 +119,8 @@ class StudyVisitEditInput(PatchInputModel): end_rule: Annotated[str | None, Field()] = None visit_contact_mode_uid: Annotated[str, Field()] epoch_allocation_uid: Annotated[str | None, Field()] = None - visit_class: Annotated[str, Field()] - visit_subclass: Annotated[str | None, Field()] = None + visit_class: Annotated[VisitClass | None, Field()] = None + visit_subclass: Annotated[VisitSubclass | None, Field()] = None is_global_anchor_visit: Annotated[bool, Field()] is_soa_milestone: Annotated[bool, Field()] = False visit_name: Annotated[str | None, Field()] = None @@ -307,9 +308,9 @@ class StudyVisitBase(BaseModel): visit_window_unit_name: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None - visit_class: Annotated[str, Field()] + visit_class: Annotated[VisitClass, Field()] visit_subclass: Annotated[ - str | None, Field(json_schema_extra={"nullable": True}) + VisitSubclass | None, Field(json_schema_extra={"nullable": True}) ] = None is_global_anchor_visit: Annotated[bool, Field()] is_soa_milestone: Annotated[bool, Field()] @@ -339,7 +340,7 @@ def transform_to_response_model( cls, visit: StudyVisitVO, study_value_version: str | None = None, - derive_props_based_on_timeline: bool = True, + derive_props_based_on_timeline: bool = False, ) -> Self: timepoint = visit.timepoint return cls( @@ -392,7 +393,11 @@ def transform_to_response_model( ), visit_subname=visit.visit_subname, visit_sublabel_reference=visit.visit_sublabel_reference, - visit_name=visit.visit_name, + visit_name=( + visit.visit_name + if derive_props_based_on_timeline + else visit.visit_name_sc.name + ), visit_short_name=( str(visit.visit_short_name) if derive_props_based_on_timeline @@ -424,8 +429,8 @@ def transform_to_response_model( start_date=visit.start_date, author_username=visit.author_username or visit.author_id, possible_actions=visit.possible_actions, - visit_class=visit.visit_class.name, - visit_subclass=visit.visit_subclass.name if visit.visit_subclass else None, + visit_class=visit.visit_class, # type: ignore[arg-type] + visit_subclass=visit.visit_subclass if visit.visit_subclass else None, is_global_anchor_visit=visit.is_global_anchor_visit, is_soa_milestone=visit.is_soa_milestone, repeating_frequency_uid=( diff --git a/clinical-mdr-api/clinical_mdr_api/models/utils.py b/clinical-mdr-api/clinical_mdr_api/models/utils.py index 0cb11fe3..4e9cab06 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/utils.py +++ b/clinical-mdr-api/clinical_mdr_api/models/utils.py @@ -328,7 +328,7 @@ class CustomPage(BaseModel, Generic[T]): """ items: Annotated[Sequence[T], Field()] - total: Annotated[int, Field(ge=0)] + total: Annotated[int, Field(ge=-1)] page: Annotated[int, Field(ge=0)] size: Annotated[int, Field(ge=0)] @@ -347,7 +347,7 @@ class GenericFilteringReturn(BaseModel, Generic[T]): """ items: Annotated[list[T], Field()] - total: Annotated[int, Field(ge=0)] + total: Annotated[int, Field(ge=-1)] @classmethod def create(cls, items: list[T], total: int) -> Self: diff --git a/clinical-mdr-api/clinical_mdr_api/models/validators.py b/clinical-mdr-api/clinical_mdr_api/models/validators.py index 3434fe89..502039eb 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/validators.py +++ b/clinical-mdr-api/clinical_mdr_api/models/validators.py @@ -8,6 +8,7 @@ from clinical_mdr_api.domains._utils import get_iso_lang_data from clinical_mdr_api.domains.concepts.utils import EN_LANGUAGE, ENG_LANGUAGE +from clinical_mdr_api.domains.enums import OdmTranslatedTextTypeEnum from common.exceptions import ValidationException FLOAT_REGEX = "^[0-9]+\\.?[0-9]*$" @@ -90,6 +91,44 @@ def validate_regex(value, info: ValidationInfo): return value +def validate_first_character_is_uppercase(value, info: ValidationInfo): + """ + Validates whether the first character of a string value is uppercase. + + Args: + cls: The class to which the field belongs. + value: The value to validate. + info: ValidationInfo + + Returns: + str: The validated value. + """ + if value and not value[0].isupper(): + raise ValueError( + f"Provided value '{value}' for '{info.field_name}' is invalid. The first character must be uppercase." + ) + return value + + +def validate_first_character_is_lowercase(value, info: ValidationInfo): + """ + Validates whether the first character of a string value is lowercase. + + Args: + cls: The class to which the field belongs. + value: The value to validate. + info: ValidationInfo + + Returns: + str: The validated value. + """ + if value and not value[0].islower(): + raise ValueError( + f"Provided value '{value}' for '{info.field_name}' is invalid. The first character must be lowercase." + ) + return value + + def transform_to_utc(value: datetime | None, info: ValidationInfo): if not value: return None @@ -122,25 +161,59 @@ def is_language_supported(value: str): return None -def has_english_description(descriptions: list[Any]): +def has_english_description(translated_texts: list[Any]): """ - Ensures that at least one description is in English (`eng` or `en`). + Ensures that there is at least one Translated Text with language English ('eng' or 'en') if Description(s) have been provided. Args: - descriptions (list[Any]): List of descriptions. + translated_texts (list[Any]): List of translated_texts. Returns: list[Any]: The original list if valid. Raises: - ValidationException: If no English description is found. + ValidationException: If no English Description is found. """ + if not translated_texts: + return [] + + descriptions = [ + tt + for tt in translated_texts + if tt.text_type == OdmTranslatedTextTypeEnum.DESCRIPTION + ] if not descriptions or any( - desc.language in {ENG_LANGUAGE, EN_LANGUAGE} for desc in descriptions + d.language in {ENG_LANGUAGE, EN_LANGUAGE} for d in descriptions ): - return descriptions + return translated_texts raise ValidationException( - msg="At least one description must be in English ('eng' or 'en')." + msg="A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." ) + + +def translated_text_uniqueness_check(translated_texts: list[Any]): + """ + Ensures that there are no duplicate Translated Texts for the same language and text_type. + + Args: + translated_texts (list[Any]): List of translated_texts. + + Returns: + list[Any]: The original list if valid. + + Raises: + ValidationException: If duplicate Translated Texts are found. + """ + seen = set() + + for translated_text in translated_texts: + identifier = (translated_text.text_type, translated_text.language) + if identifier in seen: + raise ValidationException( + msg=f"Duplicate Translated Text found for text_type '{translated_text.text_type.value}' and language '{translated_text.language}'." + ) + seen.add(identifier) + + return translated_texts diff --git a/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py b/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py index f08f1289..c10c54d2 100644 --- a/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py +++ b/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py @@ -20,7 +20,7 @@ from clinical_mdr_api.models.concepts.concept import VersionProperties from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmTranslatedTextModel, ) from clinical_mdr_api.models.controlled_terminologies.ct_term import ( SimpleDictionaryTermModel, @@ -546,6 +546,7 @@ def __init__( wildcard_properties_list: list[str] | None = None, format_filter_sort_keys: Callable | None = None, union_match_clause: str | None = None, + one_element_extra: bool = False, ): if wildcard_properties_list is None: wildcard_properties_list = [] @@ -557,6 +558,7 @@ def __init__( self.implicit_sort_by = implicit_sort_by self.page_number = page_number self.page_size = page_size + self.one_element_extra = one_element_extra self.filter_by = filter_by self.filter_operator = filter_operator self.total_count = total_count @@ -898,7 +900,7 @@ def build_filter_clause(self) -> None: ) elif ( get_field_type(attr_desc.annotation) - is OdmDescriptionModel + is OdmTranslatedTextModel ): _predicates.append( f"any(attr in {attribute} WHERE toLower(attr.name) {_parsed_operator} $wildcard_{index})" @@ -1025,11 +1027,13 @@ def build_pagination_clause(self) -> None: validate_max_skip_clause(page_number=self.page_number, page_size=self.page_size) # Set clause - self.pagination_clause = "SKIP $page_number * $page_size LIMIT $page_size" + self.pagination_clause = "SKIP $skip_number LIMIT $limit_number" # Add corresponding parameters - self.parameters["page_number"] = self.page_number - 1 - self.parameters["page_size"] = self.page_size + self.parameters["skip_number"] = (self.page_number - 1) * self.page_size + self.parameters["limit_number"] = ( + self.page_size + 1 if self.one_element_extra else self.page_size + ) def build_sort_clause(self) -> None: _sort_clause = "ORDER BY " @@ -1253,3 +1257,37 @@ def wrapper(self, *args, **kwargs): return wrapper return decorator + + +def calculate_total_count_from_query_result( + result_count: int, + page_number: int, + page_size: int, + total_count: bool, + extra_requested: bool = False, +) -> int | None: + """ + Given the number of results returned from a paginated query, + calculate the total count of items if possible. + """ + if not total_count: + # Total count not requested, return 0 + return 0 + if page_size == 0 and page_number == 1: + # All results requested in a single page + return result_count + if result_count == 0 and page_number > 1: + # No results on a page beyond the first means there may be more results + # but we cannot know how many + return None + if result_count < page_size: + # Fewer results than page size implies this is the last page + return (page_number - 1) * page_size + result_count + if extra_requested and result_count == page_size: + # We got a full page and no extra item, so we can assume this is the last page + return page_number * page_size + if extra_requested and result_count == page_size + 1: + # More results than page size implies we requested one extra item to check for more + return -1 + # Otherwise, we cannot determine the total count + return None diff --git a/clinical-mdr-api/clinical_mdr_api/routers/__init__.py b/clinical-mdr-api/clinical_mdr_api/routers/__init__.py index 70880698..25b801a7 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/__init__.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/__init__.py @@ -116,6 +116,7 @@ router as dictionary_terms_router, ) from clinical_mdr_api.routers.feature_flags import router as feature_flags_router +from clinical_mdr_api.routers.iso import router as iso_router from clinical_mdr_api.routers.libraries.libraries import router as libraries_router from clinical_mdr_api.routers.libraries.time_points import router as time_points_router from clinical_mdr_api.routers.listings.listings import metadata_router @@ -284,6 +285,7 @@ "activity_item_classes_router", "data_suppliers_router", "odm_metadata_router", + "iso_router", "compounds_router", "compound_aliases_router", "activity_subgroups_router", 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 c9f7df20..dc825ed0 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/_generic_descriptions.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/_generic_descriptions.py @@ -203,8 +203,13 @@ def _parse_json_validator(value: typing.Any) -> typing.Any: } TOTAL_COUNT = ( - "Boolean value specifying whether total count of entities should be included in the response.\n\n" - "Functionality: retrieve total count of queried entities.\n\n" + "Boolean flag to include the total count of entities in the response.\n\n" + "Default: `false`\n\n" + "Functionality: When set to `true`, returns the total number of entities that match the query.\n\n" + "When combined with filters, the count reflects only the entities matching those filters.\n\n" + "Note: This operation can be expensive for large datasets.\n\n" + "Special case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, " + "but confirms that at least one more entity exists beyond the current page." ) # Reusable Query parameter for total_count 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 40f1515a..b6dda45e 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 @@ -13,6 +13,7 @@ ActivityItemClassMappingInput, ActivityItemClassOverview, SimpleActivityInstanceClassForItem, + ValidCodelistMappingInput, ) from clinical_mdr_api.models.utils import CustomPage, GenericFilteringReturn from clinical_mdr_api.repositories._utils import FilterOperator @@ -232,6 +233,16 @@ def get_all_codelists( description="Optionally, the name of a CT Catalogue to filter Codelists." ), ] = None, + valid_codelists_for_item: Annotated[ + bool, + Query( + description=( + "Whether to look for codelists using the Valid codelists relationship.\n\n" + "if set to True, this will take precedence over SDTMIG and Sponsor Model.\n\n" + "Defaults to False." + ) + ), + ] = 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, @@ -244,6 +255,7 @@ def get_all_codelists( dataset_uid=dataset_uid, use_sponsor_model=use_sponsor_model, ct_catalogue_name=ct_catalogue_name, + valid_codelists_for_item=valid_codelists_for_item, sort_by=sort_by, page_number=page_number, page_size=page_size, @@ -387,6 +399,45 @@ def patch_mappings( ) +@router.patch( + "/{activity_item_class_uid}/valid-codelist-mappings", + dependencies=[security, rbac.LIBRARY_WRITE], + summary="Edit the mappings to valid codelists for ActivityItems", + description=""" +State before: +- uid must exist + +Business logic: +- Mappings to valid codelists are replaced with the provided ones + +Possible errors: +- Invalid uid +""", + response_model_exclude_unset=True, + status_code=200, + responses={ + 403: _generic_descriptions.ERROR_403, + 200: {"description": "OK."}, + 404: { + "model": ErrorResponse, + "description": "Not Found - Reasons include e.g.: \n" + "- The activity item class with the specified 'activity_item_class_uid' could not be found.", + }, + }, +) +def patch_valid_codelist_mappings( + activity_item_class_uid: Annotated[str, ActivityItemClassUID], + mapping_input: Annotated[ + ValidCodelistMappingInput, + Body(description="The uid of valid codelists to map activity item class to."), + ], +) -> ActivityItemClass: + activity_item_class_service = ActivityItemClassService() + return activity_item_class_service.patch_valid_codelist_mappings( + uid=activity_item_class_uid, mapping_input=mapping_input + ) + + @router.post( "/{activity_item_class_uid}/versions", dependencies=[security, rbac.LIBRARY_WRITE], 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 d1b490d7..bfa3014f 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 @@ -5,9 +5,6 @@ from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmElementWithParentUid, - OdmVendorElementRelationPostInput, - OdmVendorRelationPostInput, - OdmVendorsPostInput, ) from clinical_mdr_api.models.concepts.odms.odm_form import ( OdmForm, @@ -49,11 +46,7 @@ "oid", "library_name", "name", - "descriptions=descriptions.description", - "instructions=descriptions.instruction", - "languages=descriptions.language", - "instructions=descriptions.instruction", - "sponsor_instructions=descriptions.sponsor_instruction", + "translated_texts", "forms", "start_date", "end_date", @@ -75,7 +68,7 @@ "oid", "library_name", "name", - "descriptions", + "translated_texts", "forms", "start_date", "end_date", @@ -498,154 +491,6 @@ def add_item_groups_to_odm_form( ) -@router.post( - "/{odm_form_uid}/vendor-elements", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Elements to the ODM Form.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Elements were successfully added to the ODM Form." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Elements with the specified 'odm_form_uid' wasn't found.", - }, - }, -) -def add_vendor_elements_to_odm_form( - odm_vendor_relation_post_input: Annotated[ - list[OdmVendorElementRelationPostInput], Body() - ], - odm_form_uid: Annotated[str, OdmFormUID], - override: Annotated[ - bool, - Query( - description="If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships.", - ), - ] = False, -) -> OdmForm: - odm_form_service = OdmFormService() - return odm_form_service.add_vendor_elements( - uid=odm_form_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_form_uid}/vendor-attributes", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Attributes to the ODM Form.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Attributes were successfully added to the ODM Form." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Attributes with the specified 'odm_form_uid' wasn't found.", - }, - }, -) -def add_vendor_attributes_to_odm_form( - odm_form_uid: Annotated[str, OdmFormUID], - odm_vendor_relation_post_input: Annotated[list[OdmVendorRelationPostInput], Body()], - override: Annotated[ - bool, - Query( - description="""If true, all existing ODM Vendor Attribute relationships will - be replaced with the provided ODM Vendor Attribute relationships.""", - ), - ] = False, -) -> OdmForm: - odm_form_service = OdmFormService() - return odm_form_service.add_vendor_attributes( - uid=odm_form_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_form_uid}/vendor-element-attributes", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Element attributes to the ODM Form.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Element attributes were successfully added to the ODM Form." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Element attributes with the specified 'odm_form_uid' wasn't found.", - }, - }, -) -def add_vendor_element_attributes_to_odm_form( - odm_form_uid: Annotated[str, OdmFormUID], - odm_vendor_relation_post_input: Annotated[list[OdmVendorRelationPostInput], Body()], - override: Annotated[ - bool, - Query( - description="""If true, all existing ODM Vendor Element attribute relationships - will be replaced with the provided ODM Vendor Element attribute relationships.""", - ), - ] = False, -) -> OdmForm: - odm_form_service = OdmFormService() - return odm_form_service.add_vendor_element_attributes( - uid=odm_form_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_form_uid}/vendors", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Manages all ODM Vendors by replacing existing ODM Vendors by provided ODM Vendors.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendors were successfully added to the ODM Form." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendors with the specified 'odm_form_uid' wasn't found.", - }, - }, -) -def manage_vendors_of_odm_form( - odm_form_uid: Annotated[str, OdmFormUID], - odm_vendors_post_input: Annotated[OdmVendorsPostInput, Body()], -) -> OdmForm: - odm_form_service = OdmFormService() - return odm_form_service.manage_vendors( - uid=odm_form_uid, odm_vendors_post_input=odm_vendors_post_input - ) - - @router.delete( "/{odm_form_uid}", dependencies=[security, rbac.LIBRARY_WRITE], 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 886f26c5..2b78f658 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 @@ -5,9 +5,6 @@ from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmElementWithParentUid, - OdmVendorElementRelationPostInput, - OdmVendorRelationPostInput, - OdmVendorsPostInput, ) from clinical_mdr_api.models.concepts.odms.odm_item_group import ( OdmItemGroup, @@ -50,11 +47,7 @@ "library_name", "name", "aliases", - "descriptions=descriptions.description", - "instructions=descriptions.instruction", - "languages=descriptions.language", - "instructions=descriptions.instruction", - "sponsor_instructions=descriptions.sponsor_instruction", + "translated_texts", "repeating", "start_date", "end_date", @@ -76,7 +69,7 @@ "library_name", "name", "aliases", - "descriptions", + "translated_texts", "repeating", "start_date", "end_date", @@ -509,152 +502,6 @@ def add_item_to_odm_item_group( ) -@router.post( - "/{odm_item_group_uid}/vendor-elements", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Elements to the ODM Item Group.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Elements were successfully added to the ODM Item Group." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Elements with the specified 'odm_item_group_uid' wasn't found.", - }, - }, -) -def add_vendor_elements_to_odm_item_group( - odm_item_group_uid: Annotated[str, OdmItemGroupUID], - odm_vendor_relation_post_input: Annotated[ - list[OdmVendorElementRelationPostInput], Body() - ], - override: Annotated[ - bool, - Query( - description="If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships.", - ), - ] = False, -) -> OdmItemGroup: - odm_item_group_service = OdmItemGroupService() - return odm_item_group_service.add_vendor_elements( - uid=odm_item_group_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_item_group_uid}/vendor-attributes", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Attributes to the ODM Item Group.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Attributes were successfully added to the ODM Item Group." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Attributes with the specified 'odm_item_group_uid' wasn't found.", - }, - }, -) -def add_vendor_attributes_to_odm_item_group( - odm_item_group_uid: Annotated[str, OdmItemGroupUID], - odm_vendor_relation_post_input: Annotated[list[OdmVendorRelationPostInput], Body()], - override: Annotated[ - bool, - Query( - description="""If true, all existing ODM Vendor Attribute relationships will be replaced with the provided ODM Vendor Attribute relationships.""", - ), - ] = False, -) -> OdmItemGroup: - odm_item_group_service = OdmItemGroupService() - return odm_item_group_service.add_vendor_attributes( - uid=odm_item_group_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_item_group_uid}/vendor-element-attributes", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Element attributes to the ODM Item Group.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Element attributes were successfully added to the ODM Item Group." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Element attributes with the specified 'odm_item_group_uid' wasn't found.", - }, - }, -) -def add_vendor_element_attributes_to_odm_item_group( - odm_item_group_uid: Annotated[str, OdmItemGroupUID], - odm_vendor_relation_post_input: Annotated[list[OdmVendorRelationPostInput], Body()], - override: Annotated[ - bool, - Query( - description="""If true, all existing ODM Vendor Element attribute relationships will be replaced with the provided ODM Vendor Element attribute relationships.""", - ), - ] = False, -) -> OdmItemGroup: - odm_item_group_service = OdmItemGroupService() - return odm_item_group_service.add_vendor_element_attributes( - uid=odm_item_group_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_item_group_uid}/vendors", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Manages all ODM Vendors by replacing existing ODM Vendors by provided ODM Vendors.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendors were successfully added to the ODM Item Group." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendors with the specified 'odm_item_group_uid' wasn't found.", - }, - }, -) -def manage_vendors_of_odm_item_group( - odm_item_group_uid: Annotated[str, OdmItemGroupUID], - odm_vendors_post_input: Annotated[OdmVendorsPostInput, Body()], -): - odm_item_group_service = OdmItemGroupService() - return odm_item_group_service.manage_vendors( - uid=odm_item_group_uid, odm_vendors_post_input=odm_vendors_post_input - ) - - @router.delete( "/{odm_item_group_uid}", dependencies=[security, rbac.LIBRARY_WRITE], 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 fe51f8d0..530aab62 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 @@ -5,9 +5,6 @@ from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmElementWithParentUid, - OdmVendorElementRelationPostInput, - OdmVendorRelationPostInput, - OdmVendorsPostInput, ) from clinical_mdr_api.models.concepts.odms.odm_item import ( OdmItem, @@ -56,11 +53,7 @@ "significant_digits", "origin", "prompt", - "descriptions=descriptions.description", - "instructions=descriptions.instruction", - "languages=descriptions.language", - "instructions=descriptions.instruction", - "sponsor_instructions=descriptions.sponsor_instruction", + "translated_texts", "repeating", "start_date", "end_date", @@ -91,7 +84,7 @@ "significant_digits", "origin", "prompt", - "descriptions", + "translated_texts", "repeating", "start_date", "end_date", @@ -439,152 +432,6 @@ def reactivate_odm_item(odm_item_uid: Annotated[str, OdmItemUID]) -> OdmItem: ) -@router.post( - "/{odm_item_uid}/vendor-elements", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Elements to the ODM Item.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Elements were successfully added to the ODM Item." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Elements with the specified 'odm_item_uid' wasn't found.", - }, - }, -) -def add_vendor_elements_to_odm_item( - odm_item_uid: Annotated[str, OdmItemUID], - odm_vendor_relation_post_input: Annotated[ - list[OdmVendorElementRelationPostInput], Body() - ], - override: Annotated[ - bool, - Query( - description="If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships.", - ), - ] = False, -) -> OdmItem: - odm_item_service = OdmItemService() - return odm_item_service.add_vendor_elements( - uid=odm_item_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_item_uid}/vendor-attributes", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Attributes to the ODM Item.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Attributes were successfully added to the ODM Item." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Attributes with the specified 'odm_item_uid' wasn't found.", - }, - }, -) -def add_vendor_attributes_to_odm_item( - odm_item_uid: Annotated[str, OdmItemUID], - odm_vendor_relation_post_input: Annotated[list[OdmVendorRelationPostInput], Body()], - override: Annotated[ - bool, - Query( - description="""If true, all existing ODM Vendor Attribute relationships will be replaced with the provided ODM Vendor Attribute relationships.""", - ), - ] = False, -) -> OdmItem: - odm_item_service = OdmItemService() - return odm_item_service.add_vendor_attributes( - uid=odm_item_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_item_uid}/vendor-element-attributes", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Adds ODM Vendor Element attributes to the ODM Item.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendor Element attributes were successfully added to the ODM Item." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendor Element attributes with the specified 'odm_item_uid' wasn't found.", - }, - }, -) -def add_vendor_element_attributes_to_odm_item( - odm_item_uid: Annotated[str, OdmItemUID], - odm_vendor_relation_post_input: Annotated[list[OdmVendorRelationPostInput], Body()], - override: Annotated[ - bool, - Query( - description="""If true, all existing ODM Vendor Element attribute relationships will be replaced with the provided ODM Vendor Element attribute relationships.""", - ), - ] = False, -) -> OdmItem: - odm_item_service = OdmItemService() - return odm_item_service.add_vendor_element_attributes( - uid=odm_item_uid, - odm_vendor_relation_post_input=odm_vendor_relation_post_input, - override=override, - ) - - -@router.post( - "/{odm_item_uid}/vendors", - dependencies=[security, rbac.LIBRARY_WRITE], - summary="Manages all ODM Vendors by replacing existing ODM Vendors by provided ODM Vendors.", - status_code=201, - responses={ - 403: _generic_descriptions.ERROR_403, - 201: { - "description": "Created - The ODM Vendors were successfully added to the ODM Item." - }, - 400: { - "model": ErrorResponse, - "description": "Forbidden - Reasons include e.g.: \n", - }, - 404: { - "model": ErrorResponse, - "description": "Not Found - The ODM Vendors with the specified 'odm_item_uid' wasn't found.", - }, - }, -) -def manage_vendors_of_odm_item_group( - odm_item_uid: Annotated[str, OdmItemUID], - odm_vendors_post_input: Annotated[OdmVendorsPostInput, Body()], -) -> OdmItem: - odm_item_group_service = OdmItemService() - return odm_item_group_service.manage_vendors( - uid=odm_item_uid, odm_vendors_post_input=odm_vendors_post_input - ) - - @router.delete( "/{odm_item_uid}", dependencies=[security, rbac.LIBRARY_WRITE], 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 f59f7cfe..133dde57 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 @@ -1,10 +1,11 @@ import re from datetime import datetime -from typing import Annotated +from typing import Annotated, Literal from urllib.parse import quote -from fastapi import APIRouter, File, Path, Query, Response, UploadFile +from fastapi import APIRouter, File, Path, Query, Request, Response, UploadFile from fastapi.responses import StreamingResponse +from pydantic import StringConstraints from clinical_mdr_api.domains.concepts.utils import ExporterType, TargetType from clinical_mdr_api.models.utils import CustomPage @@ -15,10 +16,11 @@ from clinical_mdr_api.services.concepts.odms.odm_csv_exporter import ( OdmCsvExporterService, ) +from clinical_mdr_api.services.concepts.odms.odm_data_extractor import OdmDataExtractor from clinical_mdr_api.services.concepts.odms.odm_metadata import ( get_odm_aliases, - get_odm_descriptions, get_odm_formal_expressions, + get_odm_translated_texts, ) from clinical_mdr_api.services.concepts.odms.odm_xml_exporter import ( OdmXmlExporterService, @@ -29,6 +31,7 @@ from clinical_mdr_api.services.concepts.odms.odm_xml_stylesheets import ( OdmXmlStylesheetService, ) +from common import templating from common.auth import rbac from common.auth.dependencies import security from common.config import settings @@ -37,6 +40,13 @@ # Prefixed with "/concepts/odms/metadata" router = APIRouter() +OP_ANNOTATION = Annotated[ + Literal["co", "eq"], + Query( + description="Operator to use for filtering. `co` for contains, `eq` for equals. Default is `co`." + ), +] + @router.get( "/aliases", @@ -50,48 +60,69 @@ def get_aliases( page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + sort_by: Annotated[ + str, + StringConstraints(strip_whitespace=True), + Query( + description="""Comma separated list of fields to sort by. Available fields are: `name`, `context`. Prefix with `-` for descending order. + E.g. `-name,context` sorts by name in descending order and then by context in ascending order.""", + ), + ] = "", search: Annotated[ str | None, Query(description="Search by name or context. Search is case insensitive."), ] = None, + op: OP_ANNOTATION = "co", ) -> CustomPage: aliases, total = get_odm_aliases( - page_size=page_size, page_number=page_number, search=search + page_number=page_number, + page_size=page_size, + sort_by=sort_by, + search=search, + op=op, ) return CustomPage(items=aliases, size=page_size, page=page_number, total=total) @router.get( - "/descriptions", + "/translated-texts", dependencies=[security, rbac.LIBRARY_READ], - summary="Listing of ODM Descriptions", + summary="Listing of ODM Translated Texts", status_code=200, responses={ 403: _generic_descriptions.ERROR_403, }, ) -def get_descriptions( +def get_translated_texts( 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`)."), - ] = False, + sort_by: Annotated[ + str, + StringConstraints(strip_whitespace=True), + Query( + description="""Comma separated list of fields to sort by. Available fields are: `text_type`, `language` and `text`. Prefix with `-` for descending order. + E.g. `-language,text_type` sorts by language in descending order and then by text_type in ascending order.""", + ), + ] = "", search: Annotated[ str | None, Query( - description="Search by name, description, instruction or sponsor instruction. Search is case insensitive." + description="Search by text_type, language or text. Search is case insensitive." ), ] = None, + op: OP_ANNOTATION = "co", ) -> CustomPage: - descriptions, total = get_odm_descriptions( - page_size=page_size, + translated_texts, total = get_odm_translated_texts( page_number=page_number, + page_size=page_size, + sort_by=sort_by, search=search, - exclude_english=exclude_english, + op=op, ) - return CustomPage(items=descriptions, size=page_size, page=page_number, total=total) + return CustomPage( + items=translated_texts, size=page_size, page=page_number, total=total + ) @router.get( @@ -104,17 +135,30 @@ def get_descriptions( }, ) def get_formal_expressions( - page_size: _generic_descriptions.PAGE_SIZE_QUERY = 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, + sort_by: Annotated[ + str, + StringConstraints(strip_whitespace=True), + Query( + description="""Comma separated list of fields to sort by. Available fields are: `context` and `expression`. Prefix with `-` for descending order. + E.g. `-context,expression` sorts by context in descending order and then by expression in ascending order.""", + ), + ] = "", search: Annotated[ str | None, Query( description="Search by context or expression. Search is case insensitive." ), ] = None, + op: OP_ANNOTATION = "co", ) -> CustomPage: formal_expressions, total = get_odm_formal_expressions( - page_size=page_size, page_number=page_number, search=search + page_number=page_number, + page_size=page_size, + sort_by=sort_by, + search=search, + op=op, ) return CustomPage( @@ -122,6 +166,51 @@ def get_formal_expressions( ) +@router.post( + "/report", + dependencies=[security, rbac.LIBRARY_READ], + summary="Export ODM Report", + status_code=200, + responses={ + 200: { + "description": "Successful Response", + "content": {"application/html": {}}, + }, + 403: _generic_descriptions.ERROR_403, + 404: _generic_descriptions.ERROR_404, + }, + response_class=Response, +) +def get_odm_report( + request: Request, + 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.", + ), + ], +): + odm_data_extractor = OdmDataExtractor(target_type, targets) + + return templating.templates.TemplateResponse( + "odm/crf.html", + { + "request": request, + "data": { + "odm_forms": odm_data_extractor.odm_forms, + "odm_item_groups": odm_data_extractor.odm_item_groups, + "odm_items": odm_data_extractor.odm_items, + }, + }, + headers={ + "Content-Disposition": f'attachment; filename="{datetime.now().strftime('odm_report_%Y%m%d_%H%M%S.html')}"', + "X-Content-Type-Options": "nosniff", + "Content-Security-Policy": "default-src 'none'", + }, + ) + + MAPPER_DESCRIPTION = """ Optional CSV file providing mapping rules between a legacy vendor extension and its OpenStudyBuilder equivalent.\n\n Only CSV format is supported.\n\n @@ -176,14 +265,11 @@ def get_odm_document( ): if allowed_namespaces is None: allowed_namespaces = [] + odm_xml_export_service = OdmXmlExporterService( - target_type, - targets, - allowed_namespaces, - pdf, - stylesheet, - mapper_file, + target_type, targets, allowed_namespaces, pdf, stylesheet, mapper_file ) + content = odm_xml_export_service.get_odm_document() if pdf: 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 bb793d67..5743ed51 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 @@ -512,6 +512,7 @@ def add_term( term_uid=term_input.term_uid, order=term_input.order, submission_value=term_input.submission_value, + ordinal=term_input.ordinal, ) diff --git a/clinical-mdr-api/clinical_mdr_api/routers/iso.py b/clinical-mdr-api/clinical_mdr_api/routers/iso.py new file mode 100644 index 00000000..227b8ffb --- /dev/null +++ b/clinical-mdr-api/clinical_mdr_api/routers/iso.py @@ -0,0 +1,31 @@ +from typing import TypedDict + +from fastapi import APIRouter + +from clinical_mdr_api.domains.iso_languages import ( + LANGUAGES_BY_NAME_AND_639_1_AND_639_2T, +) +from common.auth import rbac +from common.auth.dependencies import security + +# Prefixed with "/iso" +router = APIRouter() + + +ISOLanguageModel = TypedDict("ISOLanguageModel", {"name": str, "_1": str, "_2T": str}) + + +@router.get( + "/639", + dependencies=[security, rbac.LIBRARY_READ_OR_STUDY_READ], + summary="Get ISO 639 Languages", + description=""" +Get a list of ISO 639 languages with their codes.\n +The list includes the language name, ISO 639-1 code and ISO 639-2T code. +""", +) +def get_iso_languages() -> list[ISOLanguageModel]: + return [ + {"name": name, "_1": x, "_2T": y} + for name, (x, y) in LANGUAGES_BY_NAME_AND_639_1_AND_639_2T.items() + ] 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 208b1a6a..4acde6a4 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study.py @@ -178,6 +178,28 @@ ) +# API endpoints to study crfs + + +@router.get( + "/studies/{study_uid}/odm-forms", + dependencies=[security, rbac.STUDY_READ], + summary="Get a paginated list of study data suppliers of a study", + status_code=200, + responses={ + 403: _generic_descriptions.ERROR_403, + }, +) +def get_a_paginated_list_of_study_crfs_of_a_study( + study_uid: Annotated[str, studyUID], +) -> list[dict[Any, Any]]: + service = StudyActivityInstanceSelectionService() + + all_items = service.get_crfs(study_uid=study_uid) + + return all_items + + # API endpoints to study data suppliers 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 a4cedf70..f117eb89 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 @@ -426,6 +426,7 @@ def export_detailed_soa_content( "defaults": [ "study_number", "study_version", + "library", "soa_group", "activity_group", "activity_subgroup", @@ -445,6 +446,7 @@ def export_detailed_soa_content( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ "Study number=study_number", "Study version=study_version", + "Library=library", "SoA group=soa_group", "Activity group=activity_group", "Activity subgroup=activity_subgroup", 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 0c54e07b..761435c8 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 @@ -124,6 +124,12 @@ def get_all( study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, + derive_props_based_on_timeline: Annotated[ + bool, + Query( + description="Indicates whether the (visit_name, visit_short_name, unique_visit_number) properties are derived based on the study visit timeline and not from database values.", + ), + ] = False, ) -> CustomPage[StudyVisit]: results = StudyVisitService.get_all_visits( study_uid=study_uid, @@ -134,6 +140,7 @@ def get_all( filter_by=filters, filter_operator=FilterOperator.from_str(operator), study_value_version=study_value_version, + derive_props_based_on_timeline=derive_props_based_on_timeline, ) return CustomPage( items=results.items, total=results.total, page=page_number, size=page_size @@ -517,11 +524,18 @@ def get_study_visit( study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, + derive_props_based_on_timeline: Annotated[ + bool, + Query( + description="Indicates whether the (visit_name, visit_short_name, unique_visit_number) properties are derived based on the study visit timeline and not from database values.", + ), + ] = False, ) -> StudyVisit: return StudyVisitService.find_by_uid( study_uid=study_uid, uid=study_visit_uid, study_value_version=study_value_version, + derive_props_based_on_timeline=derive_props_based_on_timeline, ) diff --git a/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_instance_class.py b/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_instance_class.py index 5b67eba3..1b869d72 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_instance_class.py +++ b/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_instance_class.py @@ -266,8 +266,11 @@ def get_parent_class_overview( ) # Get all versions - version_history = self.get_version_history(parent_class_uid) - all_versions = list(set(v.version for v in version_history)) + all_versions = ( + self._repos.activity_instance_class_repository.get_all_version_numbers( + parent_class_uid + ) + ) all_versions = self._sort_semantic_versions(all_versions, reverse=True) return ActivityInstanceParentClassOverview( @@ -322,8 +325,11 @@ def get_activity_instance_class_overview( ) # Get all versions - version_history = self.get_version_history(activity_instance_class_uid) - all_versions = list(set(v.version for v in version_history)) + all_versions = ( + self._repos.activity_instance_class_repository.get_all_version_numbers( + activity_instance_class_uid + ) + ) all_versions = self._sort_semantic_versions(all_versions, reverse=True) return ActivityInstanceClassOverview( 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 a483430e..4a94da8d 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 @@ -22,6 +22,7 @@ ActivityItemClassVersion, CompactActivityItemClass, SimpleActivityInstanceClassForItem, + ValidCodelistMappingInput, ) from clinical_mdr_api.models.controlled_terminologies.ct_codelist import ( CTCodelistNameAndAttributes, @@ -195,12 +196,31 @@ def patch_mappings( return self.get_by_uid(uid) + def patch_valid_codelist_mappings( + self, uid: str, mapping_input: ValidCodelistMappingInput + ) -> ActivityItemClass: + activity_item_class = self._repos.activity_item_class_repository.find_by_uid_2( + uid + ) + + NotFoundException.raise_if_not(activity_item_class, "Activity Item Class", uid) + + try: + self._repos.activity_item_class_repository.patch_valid_codelist_mappings( + uid, mapping_input.valid_codelist_uids + ) + finally: + self._repos.activity_item_class_repository.close() + + return self.get_by_uid(uid) + def get_codelists_of_activity_item_class( self, activity_item_class_uid: str, dataset_uid: str, use_sponsor_model: bool = True, ct_catalogue_name: str | None = None, + valid_codelists_for_item: bool = False, sort_by: dict[str, bool] | None = None, page_number: int = 1, page_size: int = 0, @@ -209,10 +229,18 @@ def get_codelists_of_activity_item_class( total_count: bool = False, ) -> GenericFilteringReturn[ActivityItemClassCodelist]: - codelists_and_terms = self._repos.activity_item_class_repository.get_referenced_codelist_and_term_uids( - activity_item_class_uid, dataset_uid, use_sponsor_model, ct_catalogue_name - ) - + if valid_codelists_for_item is True: + codelists_and_terms = self._repos.activity_item_class_repository.get_valid_codelists_and_terms( + activity_item_class_uid=activity_item_class_uid, + ct_catalogue_name=ct_catalogue_name, + ) + else: + codelists_and_terms = self._repos.activity_item_class_repository.get_referenced_codelist_and_term_uids( + activity_item_class_uid, + dataset_uid, + use_sponsor_model, + ct_catalogue_name, + ) if not codelists_and_terms: return EmptyGenericFilteringResult codelist_uids = codelists_and_terms.keys() 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 b1a36483..b0b3606f 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 @@ -85,6 +85,7 @@ def _create_aggregate_root( activity_item_class_name=None, ct_terms=ct_terms, unit_definitions=unit_definitions, + text_value=item.text_value, ) ) @@ -204,6 +205,7 @@ def _edit_aggregate( activity_item_class_name=None, ct_terms=ct_terms, unit_definitions=unit_definitions, + text_value=activity_item.text_value, ) ) else: 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 0197a853..64bcd73f 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 @@ -484,14 +484,27 @@ def batch_cascade_update(self, item: ActivityAR, linked_instances: dict[str, Any continue instance_from_db = linked_instances_map[activity_instance.uid] instance_groupings = [] - for grouping in item.concept_vo.activity_groupings: + if len(item.concept_vo.activity_groupings) == 1: + # If only one grouping in the activity, there is only one possible choice + # for the instance grouping, so we use it no matter if it was changed or not. + grouping = item.concept_vo.activity_groupings[0] grp = { "activity_uid": item.uid, "activity_group_uid": grouping.activity_group_uid, "activity_subgroup_uid": grouping.activity_subgroup_uid, } - if grp in instance_from_db["activity_groupings"]: - instance_groupings.append(ActivityInstanceGrouping(**grp)) + instance_groupings.append(ActivityInstanceGrouping(**grp)) + else: + # Multiple groupings in the activity, find and keep only the matching ones. + # We do not add new groupings that were not present before. + for grouping in item.concept_vo.activity_groupings: + grp = { + "activity_uid": item.uid, + "activity_group_uid": grouping.activity_group_uid, + "activity_subgroup_uid": grouping.activity_subgroup_uid, + } + if grp in instance_from_db["activity_groupings"]: + instance_groupings.append(ActivityInstanceGrouping(**grp)) if not instance_groupings: # No matching groupings found, skip this instance 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 11cc1195..aea5188c 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 @@ -14,7 +14,6 @@ from clinical_mdr_api.models.concepts.activities.activity import ( ActivityEditInput, ActivityGrouping, - SimpleActivity, ) from clinical_mdr_api.models.concepts.activities.activity_sub_group import ( ActivityGroupForActivitySubGroup, @@ -137,7 +136,7 @@ def get_activities_for_subgroup( page_number: int = 1, page_size: int = 10, total_count: bool = False, - ) -> GenericFilteringReturn[SimpleActivity]: + ): """ Get activities linked to a specific activity subgroup version with pagination. @@ -171,7 +170,7 @@ def get_activities_for_subgroup( subgroup_ar.version, ) - activities: list[SimpleActivity] = [] + activities = [] if linked_activity_data and "activities" in linked_activity_data: activity_service = ActivityService() for activity_info in linked_activity_data["activities"]: 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 87895a68..211ae018 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 @@ -304,7 +304,7 @@ def get_all_concept_versions( ] return GenericFilteringReturn(items=items, total=total) - @db.transaction + @ensure_transaction(db) def get_by_uid( self, uid: str, 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 19117a75..dea4f2fb 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 @@ -4,6 +4,7 @@ from clinical_mdr_api.models.concepts.odms.odm_form import OdmFormPostInput from clinical_mdr_api.models.concepts.odms.odm_item import ( + OdmItemCodelist, OdmItemPostInput, OdmItemTermRelationshipInput, OdmItemUnitDefinitionRelationshipInput, @@ -166,7 +167,7 @@ def _get_and_create_codelists(self): definition=codelist.getAttribute("Name"), sponsor_preferred_name=codelist.getAttribute("Name"), extensible=True, - ordinal=False, + is_ordinal=False, template_parameter=True, parent_codelist_uid=None, library_name="Sponsor", @@ -328,8 +329,6 @@ 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.get_all_concepts( filter_by={"name": {"v": [item_def.getAttribute("Name")], "op": "co"}} ).items @@ -398,23 +397,20 @@ def _get_odm_item_post_input(self, item_def): sds_var_name=item_def.getAttribute("SDSVarName"), origin=item_def.getAttribute("Origin"), comment=None, - descriptions=[ - self._create_description( - name=description["name"], - description=description["description"], - lang=description["lang"], - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input(item_def), aliases=[], unit_definitions=item_unit_definitions, - codelist_uid=codelist_uid, + codelist=( + OdmItemCodelist( + uid=codelist_uid, + ) + if codelist_uid + else None + ), terms=input_terms, ) 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.get_all_concepts( filter_by={"name": {"v": [item_group_def.getAttribute("Name")], "op": "co"}} ).items @@ -430,21 +426,12 @@ def _get_odm_item_group_post_input(self, item_group_def): purpose=item_group_def.getAttribute("Purpose"), sas_dataset_name=item_group_def.getAttribute("SASDatasetName"), comment=None, - descriptions=[ - self._create_description( - name=description["name"], - description=description["description"], - lang=description["lang"], - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input(item_group_def), aliases=[], sdtm_domain_uids=[], ) def _get_odm_form_post_input(self, form_def): - descriptions = self._extract_descriptions(form_def) - plausible_duplicates = self.odm_form_service.get_all_concepts( filter_by={"name": {"v": [form_def.getAttribute("Name")], "op": "co"}} ).items @@ -456,14 +443,7 @@ def _get_odm_form_post_input(self, form_def): ), sdtm_version="", repeating=form_def.getAttribute("Repeating"), - descriptions=[ - self._create_description( - name=description["name"], - description=description["description"], - lang=description["lang"], - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input(form_def), aliases=[], ) diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_conditions.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_conditions.py index 90eeb9b0..db11ab60 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_conditions.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_conditions.py @@ -38,7 +38,7 @@ def _create_aggregate_root( oid=concept_input.oid, name=concept_input.name, formal_expressions=concept_input.formal_expressions, - descriptions=concept_input.descriptions, + translated_texts=concept_input.translated_texts, aliases=concept_input.aliases, ), library=library, @@ -56,7 +56,7 @@ def _edit_aggregate( oid=concept_edit_input.oid, name=concept_edit_input.name, formal_expressions=concept_edit_input.formal_expressions, - descriptions=concept_edit_input.descriptions, + translated_texts=concept_edit_input.translated_texts, aliases=concept_edit_input.aliases, ), odm_object_exists_callback=self._repos.odm_condition_repository.odm_object_exists, 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 bf28bbff..90af1881 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 @@ -10,11 +10,6 @@ VendorElementCompatibleType, ) from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus -from clinical_mdr_api.models.concepts.odms.odm_common_models import ( - OdmVendorElementRelationPostInput, - OdmVendorRelationPostInput, - OdmVendorsPostInput, -) from clinical_mdr_api.models.concepts.odms.odm_form import ( OdmForm, OdmFormItemGroupPostInput, @@ -61,7 +56,7 @@ def _create_aggregate_root( name=concept_input.name, sdtm_version=concept_input.sdtm_version, repeating=strtobool(concept_input.repeating), - descriptions=concept_input.descriptions, + translated_texts=concept_input.translated_texts, aliases=concept_input.aliases, item_group_uids=[], vendor_element_uids=[], @@ -84,7 +79,7 @@ def _edit_aggregate( name=concept_edit_input.name, sdtm_version=concept_edit_input.sdtm_version, repeating=strtobool(concept_edit_input.repeating), - descriptions=concept_edit_input.descriptions, + translated_texts=concept_edit_input.translated_texts, aliases=concept_edit_input.aliases, item_group_uids=item.concept_vo.item_group_uids, vendor_element_uids=item.concept_vo.vendor_element_uids, @@ -95,6 +90,42 @@ def _edit_aggregate( ) return item + @db.transaction + def create(self, concept_input: OdmFormPostInput) -> OdmForm: + item = super().create(concept_input) + + super().manage_vendors( + item.uid, + VendorElementCompatibleType.FORM_DEF, + VendorAttributeCompatibleType.FORM_DEF, + concept_input.vendor_elements, + concept_input.vendor_element_attributes, + concept_input.vendor_attributes, + self._repos.odm_form_repository, + ) + + return self._transform_aggregate_root_to_pydantic_model( + self._repos.odm_form_repository.find_by_uid_2(item.uid) + ) + + @db.transaction + def edit_draft(self, uid: str, concept_edit_input: OdmFormPatchInput) -> OdmForm: + super().edit_draft(uid, concept_edit_input) + + super().manage_vendors( + uid, + VendorElementCompatibleType.FORM_DEF, + VendorAttributeCompatibleType.FORM_DEF, + concept_edit_input.vendor_elements, + concept_edit_input.vendor_element_attributes, + concept_edit_input.vendor_attributes, + self._repos.odm_form_repository, + ) + + return self._transform_aggregate_root_to_pydantic_model( + self._repos.odm_form_repository.find_by_uid_2(uid) + ) + @ensure_transaction(db) def add_item_groups( self, @@ -158,151 +189,6 @@ def add_item_groups( return self._transform_aggregate_root_to_pydantic_model(odm_form_ar) - @db.transaction - def add_vendor_elements( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorElementRelationPostInput], - override: bool = False, - ) -> OdmForm: - odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_form_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.are_elements_vendor_compatible( - odm_vendor_relation_post_input, VendorElementCompatibleType.FORM_DEF - ) - - if override: - self.fail_if_non_present_vendor_elements_are_used_by_current_odm_element_attributes( - odm_form_ar._concept_vo.vendor_element_attribute_uids, - odm_vendor_relation_post_input, - ) - - self._repos.odm_form_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ELEMENT, - disconnect_all=True, - ) - - for vendor_element in odm_vendor_relation_post_input: - self._repos.odm_form_repository.add_relation( - uid=uid, - relation_uid=vendor_element.uid, - relationship_type=RelationType.VENDOR_ELEMENT, - parameters={ - "value": vendor_element.value, - }, - ) - - odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_form_ar) - - @db.transaction - def add_vendor_attributes( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorRelationPostInput], - override: bool = False, - ) -> OdmForm: - odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_form_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.fail_if_these_attributes_cannot_be_added( - input_attributes=odm_vendor_relation_post_input, - compatible_type=VendorAttributeCompatibleType.FORM_DEF, - ) - - if override: - self._repos.odm_form_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ATTRIBUTE, - disconnect_all=True, - ) - - for vendor_attribute in odm_vendor_relation_post_input: - self._repos.odm_form_repository.add_relation( - uid=uid, - relation_uid=vendor_attribute.uid, - relationship_type=RelationType.VENDOR_ATTRIBUTE, - parameters={ - "value": vendor_attribute.value, - }, - ) - - odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_form_ar) - - @db.transaction - def add_vendor_element_attributes( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorRelationPostInput], - override: bool = False, - ) -> OdmForm: - odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_form_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.fail_if_these_attributes_cannot_be_added( - odm_vendor_relation_post_input, - odm_form_ar.concept_vo.vendor_element_uids, - ) - - if override: - self._repos.odm_form_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, - disconnect_all=True, - ) - - for vendor_element_attribute in odm_vendor_relation_post_input: - self._repos.odm_form_repository.add_relation( - uid=uid, - relation_uid=vendor_element_attribute.uid, - relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, - parameters={ - "value": vendor_element_attribute.value, - }, - ) - - odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_form_ar) - - def manage_vendors( - self, - uid: str, - odm_vendors_post_input: OdmVendorsPostInput, - ) -> OdmForm: - odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - self.pre_management( - uid, odm_vendors_post_input, odm_form_ar, self._repos.odm_form_repository - ) - self.add_vendor_elements(uid, odm_vendors_post_input.elements, True) - self.add_vendor_element_attributes( - uid, odm_vendors_post_input.element_attributes, True - ) - self.add_vendor_attributes(uid, odm_vendors_post_input.attributes, True) - - return self.get_by_uid(uid) - @db.transaction def get_active_relationships(self, uid: str): NotFoundException.raise_if_not( diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_generic_service.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_generic_service.py index e91c7d86..bec099f2 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_generic_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_generic_service.py @@ -13,20 +13,18 @@ from clinical_mdr_api.domain_repositories.concepts.odms.item_repository import ( ItemRepository, ) -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.concepts.odms.vendor_attribute import OdmVendorAttributeAR from clinical_mdr_api.domains.concepts.utils import ( RelationType, VendorAttributeCompatibleType, VendorElementCompatibleType, ) +from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmVendorElementRelationPostInput, OdmVendorRelationPostInput, - OdmVendorsPostInput, ) +from clinical_mdr_api.models.utils import BaseModel from clinical_mdr_api.services._utils import ensure_transaction from clinical_mdr_api.services.concepts.concept_generic_service import ( ConceptGenericService, @@ -44,10 +42,10 @@ def fail_if_non_present_vendor_elements_are_used_by_current_odm_element_attribut input_elements: list[OdmVendorElementRelationPostInput], ): """ - Raises an error if any ODM vendor element that is not present in the input is used by any of the given ODM element attributes. + Raises an error if any ODM vendor element that is not present in the input is used by any of the present ODM element attributes. Args: - attribute_uids (list[str]): The uids of the ODM element attributes. + attribute_uids (list[str]): The input ODM element attributes. input_elements (list[OdmVendorElementRelationPostInput]): The input ODM vendor elements. Returns: @@ -66,6 +64,7 @@ def fail_if_non_present_vendor_elements_are_used_by_current_odm_element_attribut odm_vendor_attribute_element_uids = { odm_vendor_attribute_ar.concept_vo.vendor_element_uid for odm_vendor_attribute_ar in odm_vendor_attribute_ars + if odm_vendor_attribute_ar.concept_vo.vendor_element_uid } BusinessLogicException.raise_if_not( @@ -307,8 +306,9 @@ def _get_odm_vendor_attributes( def pre_management( self, uid: str, - odm_vendors_post_input: OdmVendorsPostInput, - odm_ar: OdmFormAR | OdmItemGroupAR | OdmItemAR, + odm_vendor_element_post_input: list[OdmVendorElementRelationPostInput], + odm_vendor_element_attribute_post_input: list[OdmVendorRelationPostInput], + odm_ar: _AggregateRootType, repo: FormRepository | ItemGroupRepository | ItemRepository, ): """ @@ -317,7 +317,7 @@ def pre_management( Args: uid (str): The uid of the ODM form, item group, or item. odm_vendors_post_input (OdmVendorsPostInput): The ODM vendors. - odm_ar (OdmFormAR | OdmItemGroupAR | OdmItemAR): The ODM form, item group, or item. + odm_ar (_AggregateRootType): The ODM form, item group, or item. repo (FormRepository | ItemGroupRepository | ItemRepository): The repository for the ODM form, item group, or item. Returns: @@ -327,7 +327,7 @@ def pre_management( odm_ar.concept_vo.vendor_element_attribute_uids ) - { element_attribute.uid - for element_attribute in odm_vendors_post_input.element_attributes + for element_attribute in odm_vendor_element_attribute_post_input } for removed_vendor_attribute_uid in removed_vendor_attribute_uids: repo.remove_relation( @@ -337,9 +337,9 @@ def pre_management( ) new_vendor_element_uids = { - element.uid for element in odm_vendors_post_input.elements + element.uid for element in odm_vendor_element_post_input } - set(odm_ar.concept_vo.vendor_element_uids) - for element in odm_vendors_post_input.elements: + for element in odm_vendor_element_post_input: if element.uid in new_vendor_element_uids: repo.add_relation( uid=uid, @@ -462,3 +462,148 @@ def cascade_new_version(self, item): force_new_value_node=True, ignore_exc=True, ) + + @ensure_transaction(db) + def add_vendor_elements( + self, + uid: str, + odm_vendor_element_post_input: list[OdmVendorElementRelationPostInput], + odm_vendor_element_attribute_post_input: list[OdmVendorRelationPostInput], + odm_repository: FormRepository | ItemGroupRepository | ItemRepository, + override: bool = False, + ): + if override: + self.fail_if_non_present_vendor_elements_are_used_by_current_odm_element_attributes( + [ae.uid for ae in odm_vendor_element_attribute_post_input], + odm_vendor_element_post_input, + ) + + odm_repository.remove_relation( + uid=uid, + relation_uid=None, + relationship_type=RelationType.VENDOR_ELEMENT, + disconnect_all=True, + ) + + for vendor_element in odm_vendor_element_post_input: + odm_repository.add_relation( + uid=uid, + relation_uid=vendor_element.uid, + relationship_type=RelationType.VENDOR_ELEMENT, + parameters={ + "value": vendor_element.value, + }, + ) + + return self._find_by_uid_or_raise_not_found(uid) + + @ensure_transaction(db) + def add_vendor_attributes( + self, + uid: str, + odm_vendor_attribute_post_input: list[OdmVendorRelationPostInput], + odm_repository: FormRepository | ItemGroupRepository | ItemRepository, + override: bool = False, + ): + if override: + odm_repository.remove_relation( + uid=uid, + relation_uid=None, + relationship_type=RelationType.VENDOR_ATTRIBUTE, + disconnect_all=True, + ) + + for vendor_attribute in odm_vendor_attribute_post_input: + odm_repository.add_relation( + uid=uid, + relation_uid=vendor_attribute.uid, + relationship_type=RelationType.VENDOR_ATTRIBUTE, + parameters={ + "value": vendor_attribute.value, + }, + ) + + return self._find_by_uid_or_raise_not_found(uid) + + @ensure_transaction(db) + def add_vendor_element_attributes( + self, + uid: str, + odm_vendor_element_attribute_post_input: list[OdmVendorRelationPostInput], + odm_ar: _AggregateRootType, + odm_repository: FormRepository | ItemGroupRepository | ItemRepository, + override: bool = False, + ): + self.fail_if_these_attributes_cannot_be_added( + odm_vendor_element_attribute_post_input, + odm_ar.concept_vo.vendor_element_uids, + ) + + if override: + odm_repository.remove_relation( + uid=uid, + relation_uid=None, + relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, + disconnect_all=True, + ) + + for vendor_element_attribute in odm_vendor_element_attribute_post_input: + odm_repository.add_relation( + uid=uid, + relation_uid=vendor_element_attribute.uid, + relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, + parameters={ + "value": vendor_element_attribute.value, + }, + ) + + return self._find_by_uid_or_raise_not_found(uid) + + @ensure_transaction(db) + def manage_vendors( + self, + uid: str, + element_compatible_type: VendorElementCompatibleType, + attribute_compatible_type: VendorAttributeCompatibleType, + odm_vendor_element_post_input: list[OdmVendorElementRelationPostInput], + odm_vendor_element_attribute_post_input: list[OdmVendorRelationPostInput], + odm_vendor_attribute_post_input: list[OdmVendorRelationPostInput], + odm_repository: FormRepository | ItemGroupRepository | ItemRepository, + ) -> BaseModel: + odm_ar = self._find_by_uid_or_raise_not_found(uid) + + BusinessLogicException.raise_if( + odm_ar.item_metadata.status != LibraryItemStatus.DRAFT, + msg=self.OBJECT_NOT_IN_DRAFT, + ) + + self.are_elements_vendor_compatible( + odm_vendor_element_post_input, compatible_type=element_compatible_type + ) + self.fail_if_these_attributes_cannot_be_added( + odm_vendor_attribute_post_input, compatible_type=attribute_compatible_type + ) + + self.pre_management( + uid, + odm_vendor_element_post_input, + odm_vendor_element_attribute_post_input, + odm_ar, + odm_repository, + ) + + odm_ar = self.add_vendor_elements( + uid, + odm_vendor_element_post_input, + odm_vendor_element_attribute_post_input, + odm_repository, + True, + ) + self.add_vendor_element_attributes( + uid, odm_vendor_element_attribute_post_input, odm_ar, odm_repository, True + ) + self.add_vendor_attributes( + uid, odm_vendor_attribute_post_input, odm_repository, True + ) + + return self.get_by_uid(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 52da70c4..33c7d98e 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 @@ -13,11 +13,6 @@ VendorElementCompatibleType, ) from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus -from clinical_mdr_api.models.concepts.odms.odm_common_models import ( - OdmVendorElementRelationPostInput, - OdmVendorRelationPostInput, - OdmVendorsPostInput, -) from clinical_mdr_api.models.concepts.odms.odm_item_group import ( OdmItemGroup, OdmItemGroupItemPostInput, @@ -68,7 +63,7 @@ def _create_aggregate_root( origin=concept_input.origin, purpose=concept_input.purpose, comment=concept_input.comment, - descriptions=concept_input.descriptions, + translated_texts=concept_input.translated_texts, aliases=concept_input.aliases, sdtm_domain_uids=concept_input.sdtm_domain_uids, item_uids=[], @@ -97,7 +92,7 @@ def _edit_aggregate( origin=concept_edit_input.origin, purpose=concept_edit_input.purpose, comment=concept_edit_input.comment, - descriptions=concept_edit_input.descriptions, + translated_texts=concept_edit_input.translated_texts, aliases=concept_edit_input.aliases, sdtm_domain_uids=concept_edit_input.sdtm_domain_uids, item_uids=item.concept_vo.item_uids, @@ -110,6 +105,44 @@ def _edit_aggregate( ) return item + @db.transaction + def create(self, concept_input: OdmItemGroupPostInput) -> OdmItemGroup: + item = super().create(concept_input) + + super().manage_vendors( + item.uid, + VendorElementCompatibleType.ITEM_GROUP_DEF, + VendorAttributeCompatibleType.ITEM_GROUP_DEF, + concept_input.vendor_elements, + concept_input.vendor_element_attributes, + concept_input.vendor_attributes, + self._repos.odm_item_group_repository, + ) + + return self._transform_aggregate_root_to_pydantic_model( + self._repos.odm_item_group_repository.find_by_uid_2(item.uid) + ) + + @db.transaction + def edit_draft( + self, uid: str, concept_edit_input: OdmItemGroupPatchInput + ) -> OdmItemGroup: + super().edit_draft(uid, concept_edit_input) + + super().manage_vendors( + uid, + VendorElementCompatibleType.ITEM_GROUP_DEF, + VendorAttributeCompatibleType.ITEM_GROUP_DEF, + concept_edit_input.vendor_elements, + concept_edit_input.vendor_element_attributes, + concept_edit_input.vendor_attributes, + self._repos.odm_item_group_repository, + ) + + return self._transform_aggregate_root_to_pydantic_model( + self._repos.odm_item_group_repository.find_by_uid_2(uid) + ) + @ensure_transaction(db) def add_items( self, @@ -178,154 +211,6 @@ def add_items( return self._transform_aggregate_root_to_pydantic_model(odm_item_group_ar) - @db.transaction - def add_vendor_elements( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorElementRelationPostInput], - override: bool = False, - ) -> OdmItemGroup: - odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_item_group_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.are_elements_vendor_compatible( - odm_vendor_relation_post_input, VendorElementCompatibleType.ITEM_DEF - ) - - if override: - self.fail_if_non_present_vendor_elements_are_used_by_current_odm_element_attributes( - odm_item_group_ar._concept_vo.vendor_element_attribute_uids, - odm_vendor_relation_post_input, - ) - - self._repos.odm_item_group_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ELEMENT, - disconnect_all=True, - ) - - for vendor_element in odm_vendor_relation_post_input: - self._repos.odm_item_group_repository.add_relation( - uid=uid, - relation_uid=vendor_element.uid, - relationship_type=RelationType.VENDOR_ELEMENT, - parameters={ - "value": vendor_element.value, - }, - ) - - odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_item_group_ar) - - @db.transaction - def add_vendor_attributes( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorRelationPostInput], - override: bool = False, - ) -> OdmItemGroup: - odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_item_group_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.fail_if_these_attributes_cannot_be_added( - odm_vendor_relation_post_input, - compatible_type=VendorAttributeCompatibleType.ITEM_GROUP_DEF, - ) - - if override: - self._repos.odm_item_group_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ATTRIBUTE, - disconnect_all=True, - ) - - for vendor_attribute in odm_vendor_relation_post_input: - self._repos.odm_item_group_repository.add_relation( - uid=uid, - relation_uid=vendor_attribute.uid, - relationship_type=RelationType.VENDOR_ATTRIBUTE, - parameters={ - "value": vendor_attribute.value, - }, - ) - - odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_item_group_ar) - - @db.transaction - def add_vendor_element_attributes( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorRelationPostInput], - override: bool = False, - ) -> OdmItemGroup: - odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_item_group_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.fail_if_these_attributes_cannot_be_added( - odm_vendor_relation_post_input, - odm_item_group_ar.concept_vo.vendor_element_uids, - ) - - if override: - self._repos.odm_item_group_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, - disconnect_all=True, - ) - - for vendor_element_attribute in odm_vendor_relation_post_input: - self._repos.odm_item_group_repository.add_relation( - uid=uid, - relation_uid=vendor_element_attribute.uid, - relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, - parameters={ - "value": vendor_element_attribute.value, - }, - ) - - odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_item_group_ar) - - def manage_vendors( - self, - uid: str, - odm_vendors_post_input: OdmVendorsPostInput, - ) -> OdmItemGroup: - odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - self.pre_management( - uid, - odm_vendors_post_input, - odm_item_group_ar, - self._repos.odm_item_group_repository, - ) - self.add_vendor_elements(uid, odm_vendors_post_input.elements, True) - self.add_vendor_element_attributes( - uid, odm_vendors_post_input.element_attributes, True - ) - self.add_vendor_attributes(uid, odm_vendors_post_input.attributes, True) - - return self.get_by_uid(uid) - @db.transaction def get_active_relationships(self, uid: str): NotFoundException.raise_if_not( 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 1ff8ecba..9f19cc32 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 @@ -17,12 +17,6 @@ VendorAttributeCompatibleType, VendorElementCompatibleType, ) -from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus -from clinical_mdr_api.models.concepts.odms.odm_common_models import ( - OdmVendorElementRelationPostInput, - OdmVendorRelationPostInput, - OdmVendorsPostInput, -) from clinical_mdr_api.models.concepts.odms.odm_item import ( OdmItem, OdmItemPatchInput, @@ -35,7 +29,6 @@ from clinical_mdr_api.services.concepts.odms.odm_generic_service import ( OdmGenericService, ) -from clinical_mdr_api.utils import normalize_string from common.exceptions import BusinessLogicException, NotFoundException @@ -79,13 +72,13 @@ def _create_aggregate_root( sds_var_name=concept_input.sds_var_name, origin=concept_input.origin, comment=concept_input.comment, - descriptions=concept_input.descriptions, + translated_texts=concept_input.translated_texts, aliases=concept_input.aliases, unit_definition_uids=[ unit_definition.uid for unit_definition in concept_input.unit_definitions ], - codelist_uid=concept_input.codelist_uid, + codelist=concept_input.codelist, term_uids=[term.uid for term in concept_input.terms], activity_instances=[], vendor_element_uids=[], @@ -142,13 +135,13 @@ def _edit_aggregate( sds_var_name=concept_edit_input.sds_var_name, origin=concept_edit_input.origin, comment=concept_edit_input.comment, - descriptions=concept_edit_input.descriptions, + translated_texts=concept_edit_input.translated_texts, aliases=concept_edit_input.aliases, unit_definition_uids=[ unit_definition.uid for unit_definition in concept_edit_input.unit_definitions ], - codelist_uid=concept_edit_input.codelist_uid, + codelist=concept_edit_input.codelist, term_uids=[term.uid for term in concept_edit_input.terms], activity_instances=[ model.model_dump() @@ -170,8 +163,19 @@ def _edit_aggregate( def create(self, concept_input: OdmItemPostInput) -> OdmItem: item = super().create(concept_input) - self._manage_terms(item.uid, concept_input.codelist_uid, concept_input.terms) + self._manage_terms( + item.uid, getattr(concept_input.codelist, "uid", None), concept_input.terms + ) self._manage_unit_definitions(item.uid, concept_input.unit_definitions) + super().manage_vendors( + item.uid, + VendorElementCompatibleType.ITEM_DEF, + VendorAttributeCompatibleType.ITEM_DEF, + concept_input.vendor_elements, + concept_input.vendor_element_attributes, + concept_input.vendor_attributes, + self._repos.odm_item_repository, + ) return self._transform_aggregate_root_to_pydantic_model( self._repos.odm_item_repository.find_by_uid_2(item.uid) @@ -182,9 +186,21 @@ def edit_draft(self, uid: str, concept_edit_input: OdmItemPatchInput) -> OdmItem super().edit_draft(uid, concept_edit_input) self._manage_terms( - uid, concept_edit_input.codelist_uid, concept_edit_input.terms, True + uid, + getattr(concept_edit_input.codelist, "uid", None), + concept_edit_input.terms, + True, ) self._manage_unit_definitions(uid, concept_edit_input.unit_definitions, True) + super().manage_vendors( + uid, + VendorElementCompatibleType.ITEM_DEF, + VendorAttributeCompatibleType.ITEM_DEF, + concept_edit_input.vendor_elements, + concept_edit_input.vendor_element_attributes, + concept_edit_input.vendor_attributes, + self._repos.odm_item_repository, + ) return self._transform_aggregate_root_to_pydantic_model( self._repos.odm_item_repository.find_by_uid_2(uid) @@ -236,7 +252,9 @@ def inactivate_final( super().inactivate_final(uid, cascade_inactivate, force_new_value_node) - self._manage_terms(uid, old_item.concept_vo.codelist_uid, terms, True) + self._manage_terms( + uid, getattr(old_item.concept_vo.codelist, "uid", None), terms, True + ) self._manage_unit_definitions(uid, unit_definitions, True) return self._transform_aggregate_root_to_pydantic_model( @@ -289,7 +307,9 @@ def reactivate_retired( super().reactivate_retired(uid, cascade_reactivate, force_new_value_node) - self._manage_terms(uid, old_item.concept_vo.codelist_uid, terms, True) + self._manage_terms( + uid, getattr(old_item.concept_vo.codelist, "uid", None), terms, True + ) self._manage_unit_definitions(uid, unit_definitions, True) return self._transform_aggregate_root_to_pydantic_model( @@ -345,7 +365,9 @@ def create_new_version( uid, cascade_new_version, force_new_value_node, ignore_exc ) - self._manage_terms(uid, old_item.concept_vo.codelist_uid, terms, True) + self._manage_terms( + uid, getattr(old_item.concept_vo.codelist, "uid", None), terms, True + ) self._manage_unit_definitions(uid, unit_definitions, True) return self._transform_aggregate_root_to_pydantic_model( @@ -386,11 +408,19 @@ def _manage_terms( ct_term, codelist_submission_value=submission_value ) ) + props = self._repos.ct_term_attributes_repository.get_codelist_to_term_properties( + input_term.uid, codelist_uid + ) + value.has_codelist_term.connect( selected_term_node, { "mandatory": input_term.mandatory, - "order": input_term.order, + "order": ( + input_term.order + if input_term.order is not None + else props.get("order", 999999) or 999999 + ), "display_text": ( input_term.display_text if not any( @@ -465,151 +495,6 @@ def calculate_item_length_value( None, ) - @db.transaction - def add_vendor_elements( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorElementRelationPostInput], - override: bool = False, - ) -> OdmItem: - odm_item_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_item_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.are_elements_vendor_compatible( - odm_vendor_relation_post_input, VendorElementCompatibleType.ITEM_DEF - ) - - if override: - self.fail_if_non_present_vendor_elements_are_used_by_current_odm_element_attributes( - odm_item_ar._concept_vo.vendor_element_attribute_uids, - odm_vendor_relation_post_input, - ) - - self._repos.odm_item_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ELEMENT, - disconnect_all=True, - ) - - for vendor_element in odm_vendor_relation_post_input: - self._repos.odm_item_repository.add_relation( - uid=uid, - relation_uid=vendor_element.uid, - relationship_type=RelationType.VENDOR_ELEMENT, - parameters={ - "value": vendor_element.value, - }, - ) - - odm_item_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_item_ar) - - @db.transaction - def add_vendor_attributes( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorRelationPostInput], - override: bool = False, - ) -> OdmItem: - odm_item_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_item_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.fail_if_these_attributes_cannot_be_added( - odm_vendor_relation_post_input, - compatible_type=VendorAttributeCompatibleType.ITEM_DEF, - ) - - if override: - self._repos.odm_item_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ATTRIBUTE, - disconnect_all=True, - ) - - for vendor_attribute in odm_vendor_relation_post_input: - self._repos.odm_item_repository.add_relation( - uid=uid, - relation_uid=vendor_attribute.uid, - relationship_type=RelationType.VENDOR_ATTRIBUTE, - parameters={ - "value": vendor_attribute.value, - }, - ) - - odm_item_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_item_ar) - - @db.transaction - def add_vendor_element_attributes( - self, - uid: str, - odm_vendor_relation_post_input: list[OdmVendorRelationPostInput], - override: bool = False, - ) -> OdmItem: - odm_item_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - BusinessLogicException.raise_if( - odm_item_ar.item_metadata.status != LibraryItemStatus.DRAFT, - msg=self.OBJECT_NOT_IN_DRAFT, - ) - - self.fail_if_these_attributes_cannot_be_added( - odm_vendor_relation_post_input, - odm_item_ar.concept_vo.vendor_element_uids, - ) - - if override: - self._repos.odm_item_repository.remove_relation( - uid=uid, - relation_uid=None, - relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, - disconnect_all=True, - ) - - for vendor_element_attribute in odm_vendor_relation_post_input: - self._repos.odm_item_repository.add_relation( - uid=uid, - relation_uid=vendor_element_attribute.uid, - relationship_type=RelationType.VENDOR_ELEMENT_ATTRIBUTE, - parameters={ - "value": vendor_element_attribute.value, - }, - ) - - odm_item_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - return self._transform_aggregate_root_to_pydantic_model(odm_item_ar) - - def manage_vendors( - self, - uid: str, - odm_vendors_post_input: OdmVendorsPostInput, - ) -> OdmItem: - odm_item_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) - - self.pre_management( - uid, odm_vendors_post_input, odm_item_ar, self._repos.odm_item_repository - ) - self.add_vendor_elements(uid, odm_vendors_post_input.elements, True) - self.add_vendor_element_attributes( - uid, odm_vendors_post_input.element_attributes, True - ) - self.add_vendor_attributes(uid, odm_vendors_post_input.attributes, True) - - return self.get_by_uid(uid) - @ensure_transaction(db) def get_active_relationships(self, uid: str): NotFoundException.raise_if_not( 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 f39c43a1..8afb3cd1 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 @@ -3,56 +3,50 @@ from neomodel import db -from clinical_mdr_api.domains.concepts.utils import EN_LANGUAGE, ENG_LANGUAGE from common.utils import get_db_result_as_dict, validate_max_skip_clause def _query( node_name: str, fields: list[str], - page_size: int, page_number: int, + page_size: int, + sort_by: str, search: str | None, - exclude: dict[str, list[Any]] | None = None, + op: str, ) -> tuple[list[dict[str, str]], int]: """ Generic query function to get paginated results from a given node. :param node_name: Name of the node to query :param fields: List of fields to return - :param page_size: Number of items per page :param page_number: Page number + :param page_size: Number of items per page + :param sort_by: Comma separated list of fields to sort by :param search: Search term to filter results + :param op: Operator to use for filtering. :return: Tuple of list of results and total count """ - if exclude is None: - exclude = {} validate_max_skip_clause(page_number=page_number, page_size=page_size) params: dict[str, list[Any] | str | int] = {} + operator = "CONTAINS" if op == "co" else "=" + where_stmt = "" if search is not None and search.strip() != "": for key in fields: if where_stmt: - where_stmt += f"OR toLower(n.{key}) CONTAINS ${key} " + where_stmt += f"OR toLower(n.{key}) {operator} ${key} " params[key] = search.casefold() else: - where_stmt += f"WHERE (toLower(n.{key}) CONTAINS ${key} " + where_stmt += f"WHERE (toLower(n.{key}) {operator} ${key} " params[key] = search.casefold() where_stmt += ") " - for key, value in exclude.items(): - if where_stmt: - where_stmt += f"AND (NOT n.{key} IN ${key}) " - params[key] = value - else: - where_stmt += f"WHERE NOT n.{key} IN ${key} " - params[key] = value - - exclude_old = """ + exclude_old_stmt = """ MATCH (n)<--(value)<-[:LATEST]-(root) WHERE any( label IN labels(value) @@ -66,22 +60,44 @@ def _query( ) """ + if sort_by: + order_clauses = [] + for sort_field in sort_by.split(","): + sort_field = sort_field.strip() + + direction = "ASC" + field_name = sort_field + + if sort_field.startswith("-"): + direction = "DESC" + field_name = sort_field[1:] + + if field_name in fields: + order_clauses.append(f"toLower(n.{field_name}) {direction}") + + if order_clauses: + order_stmt = "ORDER BY " + ", ".join(order_clauses) + else: + order_stmt = f"ORDER BY toLower(n.{fields[0]})" + else: + order_stmt = f"ORDER BY toLower(n.{fields[0]})" + if page_size > 0: - limit = "SKIP $skip LIMIT $limit" + limit_stmt = "SKIP $skip LIMIT $limit" params["skip"] = page_size * (page_number - 1) params["limit"] = page_size else: - limit = "" + limit_stmt = "" results, columns = db.cypher_query( dedent( f""" MATCH (n:{node_name}) {where_stmt} - {exclude_old} + {exclude_old_stmt} RETURN DISTINCT {', '.join([f'n.{field} AS {field}' for field in fields])} - ORDER BY n.{fields[0]} - {limit} + {order_stmt} + {limit_stmt} """ ), params=params, @@ -89,7 +105,7 @@ def _query( total, _ = db.cypher_query( dedent( - f"MATCH (n:{node_name}) {where_stmt} {exclude_old} RETURN COUNT(DISTINCT n) as total", + f"MATCH (n:{node_name}) {where_stmt} {exclude_old_stmt} RETURN COUNT(DISTINCT n) as total", ), params=params, ) @@ -98,58 +114,87 @@ def _query( def get_odm_aliases( - page_size: int, page_number: int, search: str | None + page_number: int, + page_size: int, + sort_by: str, + search: str | None, + op: str, ) -> tuple[list[dict[str, str]], int]: """ Get all ODM Aliases. :param page_size: Number of items per page :param page_number: Page number + :param sort_by: Comma separated list of fields to sort by :param search: Search term to filter results + :param op: Operator to use for filtering. :return: List of ODM Aliases """ - return _query("OdmAlias", ["name", "context"], page_size, page_number, search) + return _query( + "OdmAlias", + ["name", "context"], + page_number, + page_size, + sort_by, + search, + op, + ) -def get_odm_descriptions( - page_size: int, page_number: int, search: str | None, exclude_english: bool = False +def get_odm_translated_texts( + page_number: int, + page_size: int, + sort_by: str, + search: str | None, + op: str, ) -> tuple[list[dict[str, str]], int]: """ - Get all ODM Descriptions. + Get all ODM Translated Texts. - :param page_size: Number of items per page∂ :param page_number: Page number + :param page_size: Number of items per page + :param sort_by: Comma separated list of fields to sort by :param search: Search term to filter results - :return: List of ODM Descriptions + :param op: Operator to use for filtering. + :return: List of ODM Translated Texts """ return _query( - "OdmDescription", - ["name", "language", "description", "instruction", "sponsor_instruction"], - page_size, + "OdmTranslatedText", + ["text_type", "language", "text"], page_number, + page_size, + sort_by, search, - {"language": [ENG_LANGUAGE, EN_LANGUAGE]} if exclude_english else None, + op, ) def get_odm_formal_expressions( - page_size: int, page_number: int, search: str | None + page_number: int, + page_size: int, + sort_by: str, + search: str | None, + op: str, ) -> tuple[list[dict[str, str]], int]: """ Get all ODM Formal Expressions. - :param page_size: Number of items per page :param page_number: Page number + :param page_size: Number of items per page + :param sort_by: Comma separated list of fields to sort by :param search: Search term to filter results + :param op: Operator to use for filtering. :return: List of ODM Formal Expressions """ return _query( "OdmFormalExpression", ["context", "expression"], - page_size, page_number, + page_size, + sort_by, search, + op, ) diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_methods.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_methods.py index 18c68730..60edc3f5 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_methods.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_methods.py @@ -36,7 +36,7 @@ def _create_aggregate_root( name=concept_input.name, method_type=concept_input.method_type, formal_expressions=concept_input.formal_expressions, - descriptions=concept_input.descriptions, + translated_texts=concept_input.translated_texts, aliases=concept_input.aliases, ), library=library, @@ -55,7 +55,7 @@ def _edit_aggregate( name=concept_edit_input.name, method_type=concept_edit_input.method_type, formal_expressions=concept_edit_input.formal_expressions, - descriptions=concept_edit_input.descriptions, + translated_texts=concept_edit_input.translated_texts, aliases=concept_edit_input.aliases, ), odm_object_exists_callback=self._repos.odm_method_repository.odm_object_exists, 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 a81b3189..72dbbd16 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, is_language_english +from clinical_mdr_api.domains._utils import get_iso_lang_data from clinical_mdr_api.domains.concepts.odms.odm_xml_definition import ( ODM, Alias, @@ -32,6 +32,10 @@ MeasurementUnitRef, MetaDataVersion, MethodDef, + OsbActivityInstance, + OsbCompletionInstructions, + OsbDesignNotes, + OsbDisplayText, OsbDomainColor, ProtocolName, Question, @@ -42,6 +46,7 @@ TranslatedText, ) from clinical_mdr_api.domains.concepts.utils import EN_LANGUAGE, TargetType +from clinical_mdr_api.domains.enums import OdmTranslatedTextTypeEnum from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmRefVendorAttributeModel, ) @@ -67,9 +72,8 @@ class OdmXmlExporterService: mapper_file: UploadFile | None = None XML_LANG = "xml:lang" + OSB_NAMESPACE = "osb" OSB_VERSION = "osb:version" - OSB_INSTRUCTION = "osb:instruction" - OSB_SPONSOR_INSTRUCTION = "osb:sponsorInstruction" SDTM_MSG_COLOURS = ["#bfffff", "#ffff96", "#96ff96", "#ffbf9c", "#ffffff"] def __init__( @@ -88,7 +92,7 @@ def __init__( target_type (TargetType): The type of the ODM element 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. + pdf (bool): 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. @@ -127,15 +131,25 @@ def __init__( ) ) - def get_odm_document(self) -> bytes: + def get_odm_document(self) -> OdmDataExtractor | bytes: """ - Gets an ODM XML document and applies a mapper file to it. + Gets an ODM XML document and optionally applies a mapper file and stylesheet transformation. + + This method generates an ODM XML document from the internal ODM representation. Depending on + the export format specified, it can return the data in different formats: + - HTML: Returns the OdmDataExtractor object directly + - XML: Returns a pretty-printed XML byte string + - PDF: Returns a PDF byte string generated by applying an XSL stylesheet transformation Returns: - Any: The generated document as a pretty-printed XML string, or as a PDF if `self.pdf` is True. + OdmDataExtractor | bytes: + - Pretty-printed XML as bytes if export_type is XML + - PDF document as bytes if export_type is PDF Raises: - BusinessLogicException: If an error occurs while generating the PDF. + BusinessLogicException: + - If stylesheet is not provided when PDF generation is requested + - If an error occurs during PDF generation (wraps the underlying exception) """ doc = self._generate_odm_xml(self.odm, self.xml_document) @@ -268,6 +282,23 @@ def _get_vendor_elements_or_empty_list( rs.append(elm) return rs + def return_if_vendor_element_is_allowed(self, element) -> Element | None: + """ + Returns the given ODM element if its namespace is allowed by `self.allowed_namespaces`. + """ + + if not element._custom_element_name: + return None + + if "*" in self.allowed_namespaces: + return element + + prefix, _ = element._custom_element_name.split(":") + if prefix in self.allowed_namespaces: + return element + + return None + def _create_vendor_attributes_of( self, target: OdmForm | OdmItemGroup | OdmItem ) -> dict[str, Attribute]: @@ -373,51 +404,85 @@ def create_odm_form_def(): name=Attribute("Name", form.name), repeating=Attribute("Repeating", form.repeating), **self._get_vendor_attributes_or_empty_dict( - { - "version": Attribute(self.OSB_VERSION, form.version), - "instruction": Attribute( - self.OSB_INSTRUCTION, - next( - ( - description.instruction - for description in form.descriptions - if is_language_english(description.language) - and description.instruction - ), - None, - ), - ), - "sponsor_instruction": Attribute( - self.OSB_SPONSOR_INSTRUCTION, - next( - ( - description.sponsor_instruction - for description in form.descriptions - if is_language_english(description.language) - and description.sponsor_instruction - ), - None, - ), - ), - } + {"version": Attribute(self.OSB_VERSION, form.version)} ), **self._create_vendor_attributes_of(form), **self._create_vendor_elements_of(form), description=Description( [ TranslatedText( - description.description, + translated_text.text, lang=Attribute( self.XML_LANG, get_iso_lang_data( # type: ignore[arg-type] - query=description.language or EN_LANGUAGE + query=translated_text.language or EN_LANGUAGE ), ), ) - for description in form.descriptions - if description.description + for translated_text in form.translated_texts + if translated_text.text_type + == OdmTranslatedTextTypeEnum.DESCRIPTION ] ), + osb_design_notes=self.return_if_vendor_element_is_allowed( + OsbDesignNotes( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in form.translated_texts + if translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DESIGN_NOTES + ] + ) + ), + osb_completion_instructions=self.return_if_vendor_element_is_allowed( + OsbCompletionInstructions( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in form.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_COMPLETION_INSTRUCTIONS + ] + ) + ), + osb_display_text=self.return_if_vendor_element_is_allowed( + OsbDisplayText( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in form.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DISPLAY_TEXT + ] + ) + ), aliases=[ Alias( name=Attribute("Name", alias.name), @@ -474,33 +539,7 @@ def create_odm_item_group_def(): ) ), **self._get_vendor_attributes_or_empty_dict( - { - "version": Attribute(self.OSB_VERSION, item_group.version), - "instruction": Attribute( - self.OSB_INSTRUCTION, - next( - ( - description.instruction - for description in item_group.descriptions - if is_language_english(description.language) - and description.instruction - ), - None, - ), - ), - "sponsor_instruction": Attribute( - self.OSB_SPONSOR_INSTRUCTION, - next( - ( - description.sponsor_instruction - for description in item_group.descriptions - if is_language_english(description.language) - and description.sponsor_instruction - ), - None, - ), - ), - } + {"version": Attribute(self.OSB_VERSION, item_group.version)} ), **self._create_vendor_attributes_of(item_group), **self._create_vendor_elements_of(item_group), @@ -521,18 +560,79 @@ def create_odm_item_group_def(): description=Description( [ TranslatedText( - description.description, + translated_text.text, lang=Attribute( self.XML_LANG, get_iso_lang_data( # type: ignore[arg-type] - query=description.language or EN_LANGUAGE, + query=translated_text.language or EN_LANGUAGE, ), ), ) - for description in item_group.descriptions - if description.description + for translated_text in item_group.translated_texts + if translated_text.text_type + == OdmTranslatedTextTypeEnum.DESCRIPTION ] ), + osb_design_notes=self.return_if_vendor_element_is_allowed( + OsbDesignNotes( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in item_group.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DESIGN_NOTES + ] + ) + ), + osb_completion_instructions=self.return_if_vendor_element_is_allowed( + OsbCompletionInstructions( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in item_group.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_COMPLETION_INSTRUCTIONS + ] + ) + ), + osb_display_text=self.return_if_vendor_element_is_allowed( + OsbDisplayText( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in item_group.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DISPLAY_TEXT + ] + ) + ), aliases=[ Alias( name=Attribute("Name", alias.name), @@ -561,107 +661,190 @@ def create_odm_item_group_def(): ] def create_odm_item_def(): - return [ - ItemDef( - oid=Attribute("OID", item.oid), - name=Attribute("Name", item.name), - origin=Attribute("Origin", item.origin), - datatype=Attribute("DataType", item.datatype.lower()), - length=Attribute("Length", item.length), - significant_digits=Attribute( - "SignificantDigits", item.significant_digits - ), - sas_field_name=Attribute("SASFieldName", item.sas_field_name), - sds_var_name=Attribute("SDSVarName", item.sds_var_name), - **self._get_vendor_attributes_or_empty_dict( - { - "version": Attribute(self.OSB_VERSION, item.version), - "instruction": Attribute( - self.OSB_INSTRUCTION, - next( - ( - description.instruction - for description in item.descriptions - if is_language_english(description.language) - and description.instruction - ), - None, - ), + item_defs: list[ItemDef] = [] + + for item in self.odm_data_extractor.odm_items: + activity_instances = [] + _seen_activity_instances: set[tuple[str, Any, Any]] = set() + + for activity_instance in item.activity_instances: + _key = ( + activity_instance.activity_instance_name or "", + activity_instance.activity_instance_adam_param_code, + activity_instance.activity_instance_topic_code, + ) + if _key in _seen_activity_instances: + continue + _seen_activity_instances.add(_key) + + activity_instances.append( + OsbActivityInstance( + _string=activity_instance.activity_instance_name or "", + adam_code=Attribute( + "osb:adamCode", + activity_instance.activity_instance_adam_param_code, ), - "sponsor_instruction": Attribute( - self.OSB_SPONSOR_INSTRUCTION, - next( - ( - description.sponsor_instruction - for description in item.descriptions - if is_language_english(description.language) - and description.sponsor_instruction - ), - None, - ), + topic_code=Attribute( + "osb:topicCode", + activity_instance.activity_instance_topic_code, ), - } - ), - **self._create_vendor_attributes_of(item), - **self._create_vendor_elements_of(item), - aliases=[ - Alias( - name=Attribute("Name", alias.name), - context=Attribute("Context", alias.context), + is_derived=Attribute("osb:isDerived", "true"), ) - for alias in item.aliases - ], - description=Description( - [ - TranslatedText( - description.description, - lang=Attribute( - self.XML_LANG, - get_iso_lang_data( # type: ignore[arg-type] - query=description.language or EN_LANGUAGE, - ), - ), + ) + + item_defs.append( + ItemDef( + oid=Attribute("OID", item.oid), + name=Attribute("Name", item.name), + origin=Attribute("Origin", item.origin), + datatype=Attribute("DataType", item.datatype.lower()), + length=Attribute("Length", item.length), + significant_digits=Attribute( + "SignificantDigits", item.significant_digits + ), + sas_field_name=Attribute("SASFieldName", item.sas_field_name), + sds_var_name=Attribute("SDSVarName", item.sds_var_name), + **self._get_vendor_attributes_or_empty_dict( + {"version": Attribute(self.OSB_VERSION, item.version)} + ), + **self._create_vendor_attributes_of(item), + **self._create_vendor_elements_of(item), + aliases=[ + Alias( + name=Attribute("Name", alias.name), + context=Attribute("Context", alias.context), ) - for description in item.descriptions - if description.description - ] - ), - question=Question( - [ - TranslatedText( - description.name, - lang=Attribute( - self.XML_LANG, - get_iso_lang_data( # type: ignore[arg-type] - query=description.language or EN_LANGUAGE, + for alias in item.aliases + ], + description=Description( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), ), - ), + ) + for translated_text in item.translated_texts + if translated_text.text_type + == OdmTranslatedTextTypeEnum.DESCRIPTION + ] + ), + question=Question( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in item.translated_texts + if translated_text.text_type + == OdmTranslatedTextTypeEnum.QUESTION + ] + ), + osb_design_notes=self.return_if_vendor_element_is_allowed( + OsbDesignNotes( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in item.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DESIGN_NOTES + ] ) - for description in item.descriptions - if description.name - ] - ), - codelist_ref=CodeListRef( - codelist_oid=Attribute( - "CodeListOID", - ( - f"{item.codelist.submission_value}@{item.oid}" - if item.codelist - else None + ), + osb_completion_instructions=self.return_if_vendor_element_is_allowed( + OsbCompletionInstructions( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in item.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_COMPLETION_INSTRUCTIONS + ] + ) + ), + osb_display_text=self.return_if_vendor_element_is_allowed( + OsbDisplayText( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in item.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DISPLAY_TEXT + ] + ) + ), + activity_instances=( + self._get_vendor_elements_or_empty_list(activity_instances) + ), + codelist_ref=CodeListRef( + codelist_oid=Attribute( + "CodeListOID", + ( + f"{item.codelist.submission_value}@{item.oid}" + if item.codelist + else None + ), ), - ) - ), - measurement_unit_refs=[ - MeasurementUnitRef( - measurement_unit_oid=Attribute( - "MeasurementUnitOID", unit_definition.uid + **self._get_vendor_attributes_or_empty_dict( + { + "osb_allows_multi_choice": Attribute( + "osb:allowsMultiChoice", + getattr( + item.codelist, "allows_multi_choice", False + ), + ) + } + ), + ), + measurement_unit_refs=[ + MeasurementUnitRef( + measurement_unit_oid=Attribute( + "MeasurementUnitOID", unit_definition.uid + ) ) - ) - for unit_definition in item.unit_definitions - ], + for unit_definition in item.unit_definitions + ], + ) ) - for item in self.odm_data_extractor.odm_items - ] + + return item_defs def create_odm_condition_def(): return [ @@ -688,18 +871,79 @@ def create_odm_condition_def(): description=Description( [ TranslatedText( - description.description, + translated_text.text, lang=Attribute( self.XML_LANG, get_iso_lang_data( # type: ignore[arg-type] - query=description.language or EN_LANGUAGE, + query=translated_text.language or EN_LANGUAGE, ), ), ) - for description in condition.descriptions - if description.description + for translated_text in condition.translated_texts + if translated_text.text_type + == OdmTranslatedTextTypeEnum.DESCRIPTION ] ), + osb_design_notes=self.return_if_vendor_element_is_allowed( + OsbDesignNotes( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in condition.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DESIGN_NOTES + ] + ) + ), + osb_completion_instructions=self.return_if_vendor_element_is_allowed( + OsbCompletionInstructions( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in condition.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_COMPLETION_INSTRUCTIONS + ] + ) + ), + osb_display_text=self.return_if_vendor_element_is_allowed( + OsbDisplayText( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in condition.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DISPLAY_TEXT + ] + ) + ), ) for condition in self.odm_data_extractor.odm_conditions ] @@ -730,18 +974,79 @@ def create_odm_method_def(): description=Description( [ TranslatedText( - description.description, + translated_text.text, lang=Attribute( self.XML_LANG, get_iso_lang_data( # type: ignore[arg-type] - query=description.language or EN_LANGUAGE, + query=translated_text.language or EN_LANGUAGE, ), ), ) - for description in method.descriptions - if description.description + for translated_text in method.translated_texts + if translated_text.text_type + == OdmTranslatedTextTypeEnum.DESCRIPTION ] ), + osb_design_notes=self.return_if_vendor_element_is_allowed( + OsbDesignNotes( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in method.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DESIGN_NOTES + ] + ) + ), + osb_completion_instructions=self.return_if_vendor_element_is_allowed( + OsbCompletionInstructions( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in method.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_COMPLETION_INSTRUCTIONS + ] + ) + ), + osb_display_text=self.return_if_vendor_element_is_allowed( + OsbDisplayText( + [ + TranslatedText( + translated_text.text, + lang=Attribute( + self.XML_LANG, + get_iso_lang_data( # type: ignore[arg-type] + query=translated_text.language + or EN_LANGUAGE, + ), + ), + ) + for translated_text in method.translated_texts + if self.OSB_NAMESPACE in self.allowed_namespaces + and translated_text.text_type + == OdmTranslatedTextTypeEnum.OSB_DISPLAY_TEXT + ] + ) + ), ) for method in self.odm_data_extractor.odm_methods ] 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 58fdb114..ea61d2f3 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,22 +8,23 @@ from clinical_mdr_api.domain_repositories.concepts.odms.odm_generic_repository import ( OdmGenericRepository, ) -from clinical_mdr_api.domains._utils import get_iso_lang_data, is_language_english +from clinical_mdr_api.domains._utils import get_iso_lang_data from clinical_mdr_api.domains.concepts.utils import ( EN_LANGUAGE, RelationType, VendorAttributeCompatibleType, VendorElementCompatibleType, ) +from clinical_mdr_api.domains.enums import OdmTranslatedTextTypeEnum from clinical_mdr_api.domains.versioned_object_aggregate import ( LibraryItemStatus, LibraryVO, ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, OdmFormalExpressionModel, OdmRefVendorPostInput, + OdmTranslatedTextModel, OdmVendorElementRelationPostInput, OdmVendorRelationPostInput, ) @@ -38,6 +39,7 @@ ) from clinical_mdr_api.models.concepts.odms.odm_item import ( OdmItem, + OdmItemCodelist, OdmItemPostInput, OdmItemTermRelationshipInput, OdmItemUnitDefinitionRelationshipInput, @@ -162,15 +164,13 @@ class OdmXmlImporterService: mapper_file: UploadFile | None = None OSB_PREFIX = "osb" - EXCLUDED_OSB_VENDOR_ATTRIBUTES = [ - "version", - "lang", - "instruction", - "sponsorInstruction", + EXCLUDED_OSB_VENDOR_ATTRIBUTES = ["version", "lang", "allowsMultiChoice"] + EXCLUDED_OSB_VENDOR_ELEMENTS = [ + "DomainColor", + "DisplayText", + "DesignNotes", + "CompletionInstructions", ] - EXCLUDED_OSB_VENDOR_ELEMENTS = ["DomainColor"] - OSB_INSTRUCTION = f"{OSB_PREFIX}:instruction" - OSB_SPONSOR_INSTRUCTION = f"{OSB_PREFIX}:sponsorInstruction" def __init__(self, xml_file: UploadFile, mapper_file: UploadFile | None): exceptions.BusinessLogicException.raise_if( @@ -759,8 +759,6 @@ def _set_ct_term_attributes(self): def _create_conditions_with_relations(self): for condition_def in self.condition_defs: - descriptions = self._extract_descriptions(condition_def) - rs = self._create( self._repos.odm_condition_repository, self.odm_condition_service, @@ -777,14 +775,9 @@ def _create_conditions_with_relations(self): "FormalExpression" ) ], - descriptions=[ - self._create_description( - name=description["name"], - lang=description["lang"], - description=description["description"], - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input( + condition_def + ), aliases=[ OdmAliasModel( name=alias_element.getAttribute("Name"), @@ -800,8 +793,6 @@ def _create_conditions_with_relations(self): def _create_methods_with_relations(self): for method_def in self.method_defs: - descriptions = self._extract_descriptions(method_def) - rs = self._create( self._repos.odm_method_repository, self.odm_method_service, @@ -819,14 +810,7 @@ def _create_methods_with_relations(self): "FormalExpression" ) ], - descriptions=[ - self._create_description( - name=description["name"], - lang=description["lang"], - description=description["description"], - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input(method_def), aliases=[ OdmAliasModel( name=alias_element.getAttribute("Name"), @@ -855,7 +839,9 @@ def _create_items_with_relations(self): if odm_item_post_input.terms: self.odm_item_service._manage_terms( - rs.uid, odm_item_post_input.codelist_uid, odm_item_post_input.terms + rs.uid, + getattr(odm_item_post_input.codelist, "uid", None), + odm_item_post_input.terms, ) self.odm_item_service._manage_unit_definitions( rs.uid, odm_item_post_input.unit_definitions @@ -1040,83 +1026,31 @@ def _create_study_event_with_relations(self): self._repos.odm_study_event_repository, self.odm_study_event_service, rs ) - def _create_description( - self, - name: str | minidom.Text, - lang: str = EN_LANGUAGE, - description: str | None = None, - instruction: str | None = None, - sponsor_instruction: str | None = None, - ) -> OdmDescriptionModel: - if isinstance(name, minidom.Text): - name = name.nodeValue - - if not description: - description = "Please update this description" - - if not instruction: - instruction = "Please update this instruction" - - if not sponsor_instruction: - sponsor_instruction = "Please update this sponsor instruction" - - return OdmDescriptionModel( - name=str(name) or "TBD", - language=lang, - description=description, - instruction=instruction if is_language_english(lang) else None, - sponsor_instruction=( - sponsor_instruction if is_language_english(lang) else None - ), - ) + def _get_translated_text_post_input(self, elm): + translated_texts: list[OdmTranslatedTextModel] = [] - def _extract_descriptions(self, elm): - description_element = elm.getElementsByTagName("Description") - question_element = elm.getElementsByTagName("Question") - descriptions = [] - description_langs = [] + for translated_text_type in OdmTranslatedTextTypeEnum: + translated_text_parent_element = elm.getElementsByTagName( + translated_text_type.value + ) - if description_element: - for translated_text in description_element[0].getElementsByTagName( - "TranslatedText" - ): - lang = translated_text.getAttribute("xml:lang") - desc = translated_text.firstChild.nodeValue - - descriptions.append( - { - "lang": lang, - "name": desc, - "description": desc, - } - ) - description_langs.append(lang) + if not translated_text_parent_element: + continue - if question_element: - for translated_text in question_element[0].getElementsByTagName( - "TranslatedText" - ): - lang = translated_text.getAttribute("xml:lang") - name = translated_text.firstChild - - if lang not in description_langs: - descriptions.append( - { - "lang": lang, - "name": name, - "description": None, - } + for translated_text in translated_text_parent_element[ + 0 + ].getElementsByTagName("TranslatedText"): + translated_texts.append( + OdmTranslatedTextModel( + text_type=translated_text_type, + language=get_iso_lang_data( # type: ignore[arg-type] + translated_text.getAttribute("xml:lang") or EN_LANGUAGE + ), + text=str(translated_text.firstChild.nodeValue) or "TBD", ) - else: - for description in descriptions: - if lang == description["lang"]: - description["name"] = name - break - - for description in descriptions: - description["lang"] = get_iso_lang_data(description["lang"] or EN_LANGUAGE) + ) - return descriptions + return translated_texts def _get_newly_created_vendor_namespaces(self): rs, _ = self._repos.odm_vendor_namespace_repository.find_all( @@ -1367,13 +1301,16 @@ def _get_codelist_description_translatedtext_value(codelist): ) from exc def _get_odm_item_post_input(self, item_def): - descriptions = self._extract_descriptions(item_def) - item_unit_definitions = self._get_item_unit_definition_inputs(item_def) - codelist = next( + codelist, codelist_allows_multi_choice = next( ( - codelist + ( + codelist, + item_def.getElementsByTagName("CodeListRef")[0].getAttribute( + "osb:allowsMultiChoice" + ), + ) for codelist in self.codelists if item_def.getElementsByTagName("CodeListRef") and codelist.getAttribute("OID") @@ -1381,7 +1318,7 @@ def _get_odm_item_post_input(self, item_def): "CodeListOID" ) ), - None, + (None, False), ) input_terms = [] @@ -1389,6 +1326,9 @@ def _get_odm_item_post_input(self, item_def): new_codelist = False if codelist: codelist_name = codelist.getAttribute("Name") + codelist_allows_multi_choice = ( + codelist_allows_multi_choice.lower() == "true" + ) codelist_uid = ( self._repos.ct_codelist_attribute_repository.find_uid_by_name( codelist_name @@ -1407,7 +1347,7 @@ def _get_odm_item_post_input(self, item_def): nci_preferred_name=codelist_description_translatedtext, definition=codelist_description_translatedtext, extensible=True, - ordinal=False, + is_ordinal=False, sponsor_preferred_name=codelist_name, template_parameter=False, terms=[], @@ -1469,7 +1409,7 @@ def _get_odm_item_post_input(self, item_def): self.ct_codelist_service.add_term( codelist_uid=codelist_uid, term_uid=term_uid, - order=999999, + order=None, submission_value=coded_value_value, ) @@ -1481,7 +1421,7 @@ def _get_odm_item_post_input(self, item_def): if codelist_item.getAttribute("osb:mandatory") != "" else True ), - order=codelist_item.getAttribute("OrderNumber"), + order=codelist_item.getAttribute("OrderNumber") or None, display_text=codelist_item.getElementsByTagName( "TranslatedText" )[0].firstChild.nodeValue, @@ -1498,18 +1438,7 @@ def _get_odm_item_post_input(self, item_def): sas_field_name=item_def.getAttribute("SASFieldName"), sds_var_name=item_def.getAttribute("SDSVarName"), origin=item_def.getAttribute("Origin"), - descriptions=[ - self._create_description( - name=description["name"], - lang=description["lang"], - description=description["description"], - instruction=item_def.getAttribute(self.OSB_INSTRUCTION), - sponsor_instruction=item_def.getAttribute( - self.OSB_SPONSOR_INSTRUCTION - ), - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input(item_def), aliases=[ OdmAliasModel( name=alias_element.getAttribute("Name"), @@ -1518,12 +1447,17 @@ def _get_odm_item_post_input(self, item_def): for alias_element in item_def.getElementsByTagName("Alias") ], unit_definitions=item_unit_definitions, - codelist_uid=codelist_uid, + codelist=( + OdmItemCodelist( + uid=codelist_uid, allows_multi_choice=codelist_allows_multi_choice + ) + if codelist_uid + else None + ), terms=input_terms, ) def _get_odm_item_group_post_input(self, item_group_def): - descriptions = self._extract_descriptions(item_group_def) sdtm_domain_uids = [] for domain in item_group_def.getAttribute("Domain").split("|"): if domain: @@ -1558,18 +1492,7 @@ def _get_odm_item_group_post_input(self, item_group_def): is_reference_data="no", # missing in odm purpose=item_group_def.getAttribute("Purpose"), sas_dataset_name=item_group_def.getAttribute("SASDatasetName"), - descriptions=[ - self._create_description( - name=description["name"], - lang=description["lang"], - description=description["description"], - instruction=item_group_def.getAttribute(self.OSB_INSTRUCTION), - sponsor_instruction=item_group_def.getAttribute( - self.OSB_SPONSOR_INSTRUCTION - ), - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input(item_group_def), aliases=[ OdmAliasModel( name=alias_element.getAttribute("Name"), @@ -1581,25 +1504,12 @@ 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) - return OdmFormPostInput( oid=form_def.getAttribute("OID"), name=form_def.getAttribute("Name"), sdtm_version="", repeating=form_def.getAttribute("Repeating"), - descriptions=[ - self._create_description( - name=description["name"], - lang=description["lang"], - description=description["description"], - instruction=form_def.getAttribute(self.OSB_INSTRUCTION), - sponsor_instruction=form_def.getAttribute( - self.OSB_SPONSOR_INSTRUCTION - ), - ) - for description in descriptions - ], + translated_texts=self._get_translated_text_post_input(form_def), aliases=[ OdmAliasModel( name=alias_element.getAttribute("Name"), 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 88d2d29d..8039e49f 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 @@ -98,7 +98,7 @@ def create( preferred_term=codelist_input.nci_preferred_name, definition=codelist_input.definition, extensible=codelist_input.extensible, - ordinal=codelist_input.ordinal, + is_ordinal=codelist_input.is_ordinal, catalogue_exists_callback=self._repos.ct_catalogue_repository.catalogue_exists, codelist_exists_by_uid_callback=self._repos.ct_codelist_attribute_repository.codelist_specific_exists_by_uid, codelist_exists_by_name_callback=self._repos.ct_codelist_attribute_repository.codelist_specific_exists_by_name, @@ -205,6 +205,7 @@ def create( author_id=self.author_id, order=term.order, submission_value=term.submission_value, + ordinal=term.ordinal, ) if ct_codelist_name_ar is None or ct_codelist_attributes_ar is None: @@ -355,7 +356,12 @@ def get_distinct_values_for_header( @ensure_transaction(db) def add_term( - self, codelist_uid: str, term_uid: str, order: int | None, submission_value: str + self, + codelist_uid: str, + term_uid: str, + order: int | None, + submission_value: str, + ordinal: float | None = None, ) -> CTCodelist: ct_codelist_attributes_ar = ( self._repos.ct_codelist_attribute_repository.find_by_uid( @@ -370,9 +376,16 @@ def add_term( msg=f"Codelist with UID '{codelist_uid}' isn't extensible.", ) - if ct_codelist_attributes_ar.ct_codelist_vo.ordinal and order is None: + if ct_codelist_attributes_ar.ct_codelist_vo.is_ordinal and ordinal is None: + raise BusinessLogicException( + msg=f"Codelist identified by {codelist_uid} is ordinal, therefore term ordinal value is required" + ) + if ( + ordinal is not None + and not ct_codelist_attributes_ar.ct_codelist_vo.is_ordinal + ): raise BusinessLogicException( - msg=f"Codelist identified by {codelist_uid} is ordinal and order is required" + msg=f"Codelist identified by {codelist_uid} is not ordinal, therefore term ordinal value should be None" ) parent_codelist_uid = ( @@ -459,6 +472,7 @@ def add_term( author_id=self.author_id, order=order, submission_value=submission_value, + ordinal=ordinal, ) if ct_codelist_attributes_ar is None or ct_codelist_name_ar is None: diff --git a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_attributes.py b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_attributes.py index 70ecb885..29fab9f5 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_attributes.py @@ -54,8 +54,8 @@ def edit_draft(self, codelist_uid: str, codelist_input: BaseModel) -> BaseModel: extensible=self.get_input_or_previous_property( codelist_input.extensible, item.ct_codelist_vo.extensible ), - ordinal=self.get_input_or_previous_property( - codelist_input.ordinal, item.ct_codelist_vo.ordinal + is_ordinal=self.get_input_or_previous_property( + codelist_input.is_ordinal, item.ct_codelist_vo.is_ordinal ), # passing always True callbacks, as we can't change catalogue # in scope of CodelistName or CodelistAttributes, it can be only changed via CTCodelistRoot 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 aafabd21..d859805f 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 @@ -111,11 +111,19 @@ def create( ) if ( ct_codelist_attributes_ar is not None - and ct_codelist_attributes_ar.ct_codelist_vo.ordinal - and codelist.order is None + and ct_codelist_attributes_ar.ct_codelist_vo.is_ordinal + and codelist.ordinal is None ): raise exceptions.BusinessLogicException( - msg=f"Codelist identified by {codelist.codelist_uid} is ordinal and order is required" + msg=f"Codelist identified by {codelist.codelist_uid} is ordinal, therefore term ordinal value is required" + ) + if ( + ct_codelist_attributes_ar is not None + and not ct_codelist_attributes_ar.ct_codelist_vo.is_ordinal + and codelist.ordinal is not None + ): + raise BusinessLogicException( + msg=f"Codelist identified by {codelist.codelist_uid} is not ordinal, therefore term ordinal value should be None" ) ct_codelist_name_ar = self._repos.ct_codelist_name_repository.find_by_uid( codelist_uid=codelist.codelist_uid @@ -129,6 +137,7 @@ def create( CTTermCodelistVO( codelist_uid=codelist.codelist_uid, order=codelist.order, + ordinal=codelist.ordinal, submission_value=codelist.submission_value, library_name=ct_codelist_name_ar.library.name, codelist_submission_value=ct_codelist_attributes_ar.ct_codelist_vo.submission_value, @@ -187,6 +196,7 @@ def create( author_id=self.author_id, order=cl.order, submission_value=cl.submission_value, + ordinal=cl.ordinal, ) codelists_vo = CTTermVO(codelists, []) @@ -333,6 +343,7 @@ def add_parent( CTTermCodelistVO( codelist_uid=codelist.codelist_uid, order=codelist.order, + ordinal=codelist.ordinal, submission_value=codelist.submission_value, library_name=codelist.library_name, codelist_submission_value=codelist.codelist_submission_value, @@ -395,6 +406,7 @@ def remove_parent( CTTermCodelistVO( codelist_uid=codelist.codelist_uid, order=codelist.order, + ordinal=codelist.ordinal, submission_value=codelist.submission_value, library_name=codelist.library_name, codelist_submission_value=codelist.codelist_submission_value, diff --git a/clinical-mdr-api/clinical_mdr_api/services/ctr_xml/ctr_xml_service.py b/clinical-mdr-api/clinical_mdr_api/services/ctr_xml/ctr_xml_service.py index ed1760b0..08e6b0f3 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/ctr_xml/ctr_xml_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/ctr_xml/ctr_xml_service.py @@ -244,10 +244,10 @@ def get_odm_form_defs(self) -> list[ctrxml.FormDef]: description=ctrxml.Description( translated_text=[ ctrxml.TranslatedText( - value=description.description or "", + value=description.text or "", lang=iso639_shortest(description.language or "en"), ) - for description in form.descriptions + for description in form.translated_texts ] ), item_group_ref=[ @@ -305,10 +305,10 @@ def get_odm_item_group_defs(self) -> list[ctrxml.ItemGroupDef]: description=ctrxml.Description( translated_text=[ ctrxml.TranslatedText( - value=description.description or "", + value=description.text or "", lang=iso639_shortest(description.language or "en"), ) - for description in item_group.descriptions + for description in item_group.translated_texts ] ), item_ref=[ 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 8f5aa8cb..546a1198 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 @@ -295,8 +295,9 @@ def map(self, study: OSBStudy) -> dict[str, Any]: titles=[ddf_study_title], studyIdentifiers=self._get_study_identifiers(study), versionIdentifier="", - rationale="", + rationale="Missing metadata", instanceType="StudyVersion", + documentVersionIds=[s.id for s in usdm_study.documentedBy], ) # Set DDF study version identifier @@ -1109,13 +1110,20 @@ def _get_study_type(self, study: OSBStudy): @trace_calls def _get_study_version(self, study: OSBStudy): osb_current_metadata = getattr(study, "current_metadata", None) - return str( - getattr( - getattr(osb_current_metadata, "version_metadata", None), - "version_number", - "", - ) - ) + if not osb_current_metadata: + return "" + + if osb_version_metadata := getattr( + osb_current_metadata, "version_metadata", None + ): + + rs = osb_version_metadata.study_status + if osb_version_metadata.version_number: + rs += f" v{osb_version_metadata.version_number}" + + return rs + + return "" @trace_calls def _get_study_encounters(self, study: OSBStudy): diff --git a/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset.py b/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset.py index a5c98723..d08575ae 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset.py +++ b/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset.py @@ -55,6 +55,7 @@ def _create_aggregate_root( label=item_input.label, state=item_input.state, extended_domain=item_input.extended_domain, + extra_properties=item_input.get_extra_fields(), ), library=library, ) @@ -90,6 +91,7 @@ def _edit_aggregate( label=item_edit_input.label, state=item_edit_input.state, extended_domain=item_edit_input.extended_domain, + extra_properties=item_edit_input.get_extra_fields(), ), ) return item diff --git a/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset_variable.py b/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset_variable.py index dfa0a7e1..d59ad3ea 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset_variable.py +++ b/clinical-mdr-api/clinical_mdr_api/services/standard_data_models/sponsor_model_dataset_variable.py @@ -71,6 +71,7 @@ def _create_aggregate_root( value_lvl_ct_codelist_id_col=item_input.value_lvl_ct_codelist_id_col, enrich_build_order=item_input.enrich_build_order, enrich_rule=item_input.enrich_rule, + extra_properties=item_input.get_extra_fields(), ), library=library, ) @@ -122,6 +123,7 @@ def _edit_aggregate( value_lvl_ct_codelist_id_col=item_edit_input.value_lvl_ct_codelist_id_col, enrich_build_order=item_edit_input.enrich_build_order, enrich_rule=item_edit_input.enrich_rule, + extra_properties=item_edit_input.get_extra_fields(), ), ) return item 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 4eb0d649..174ebbc9 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study.py @@ -90,6 +90,7 @@ from common.auth.user import user from common.config import settings from common.exceptions import ( + AlreadyExistsException, BusinessLogicException, NotFoundException, ValidationException, @@ -516,13 +517,36 @@ def lock(self, uid: str, change_description: str) -> Study: ] if not sponsor_ct_package_with_latest_ct_package: # create sponsor ct_package with today's date - sponsor_ct_package_with_latest_ct_package = ( - self._repos.ct_package_repository.create_sponsor_package( - extends_package=all_ct_packages[-1].uid, - effective_date=date.today(), - author_id=self.author_id, + # Handle race condition: if another process creates the package concurrently, + # catch AlreadyExistsException and fetch the existing package + try: + sponsor_ct_package_with_latest_ct_package = ( + self._repos.ct_package_repository.create_sponsor_package( + extends_package=all_ct_packages[-1].uid, + effective_date=date.today(), + author_id=self.author_id, + ) + ) + except AlreadyExistsException as exc: + # Package was created by another process, fetch the existing one + sponsor_ct_package_with_latest_ct_package = [ + ith + for ith in self._repos.ct_package_repository.find_all( + catalogue_name=settings.sdtm_ct_catalogue_name, + standards_only=True, + sponsor_only=True, + ) + if ith.effective_date == date.today() + ] + # Should have exactly one package now + if not sponsor_ct_package_with_latest_ct_package: + raise NotFoundException( + "Sponsor CT Package", + f"Sponsor {settings.sdtm_ct_catalogue_name} {date.today()}", + ) from exc + sponsor_ct_package_with_latest_ct_package = ( + sponsor_ct_package_with_latest_ct_package[0] ) - ) # create study standard version self._repos.study_standard_version_repository.save( StudyStandardVersionVO( 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 7b1948ff..6eb76910 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 @@ -42,6 +42,7 @@ from clinical_mdr_api.services.studies.study_activity_selection_base import ( StudyActivitySelectionBaseService, ) +from clinical_mdr_api.utils import db_result_to_list from common import exceptions from common.auth.user import user @@ -605,3 +606,26 @@ def handle_review_changes( ) ) return results + + def get_crfs(self, study_uid: str): + query = """ + MATCH (sr:StudyRoot {uid: $study_uid})-[:LATEST]->(:StudyValue) + -[:HAS_STUDY_ACTIVITY_INSTANCE]->(:StudyActivityInstance) + -[:HAS_SELECTED_ACTIVITY_INSTANCE]->(:ActivityInstanceValue) + -[:CONTAINS_ACTIVITY_ITEM]->(:ActivityItem) + <-[:LINKS_TO_ACTIVITY_ITEM]-(ofv:OdmFormValue) + <-[hv:HAS_VERSION]-(ofr:OdmFormRoot) + + WITH ofr, ofv, hv ORDER BY hv.end_date DESC + + WITH ofr, COLLECT({ofv: ofv, hv: hv})[0] AS latest + + RETURN DISTINCT + ofr.uid AS uid, + latest.ofv.name AS name, + latest.hv.version AS version + """ + + results = db.cypher_query(query, params={"study_uid": study_uid}) + + return db_result_to_list(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 d492df02..64028336 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 @@ -650,6 +650,7 @@ def _create_value_object( generate_uid_callback=self.repository.generate_uid, activity_subgroup_uid=selection_create_input.activity_subgroup_uid, activity_group_uid=selection_create_input.activity_group_uid, + show_activity_in_protocol_flowchart=selection_create_input.show_activity_in_protocol_flowchart, ) return new_selection @@ -808,7 +809,7 @@ def _validate_activity_subgroup( activity_subgroup_uid, version=activity_subgroup_version ) NotFoundException.raise_if_not( - activity_subgroup_uid, "Activity Subgroup", activity_subgroup_uid + activity_subgroup_ar, "Activity Subgroup", activity_subgroup_uid ) NotFoundException.raise_if( @@ -837,7 +838,7 @@ def _validate_activity_group( ) NotFoundException.raise_if_not( - activity_group_uid, "Activity Group", activity_group_uid + activity_group_ar, "Activity Group", activity_group_uid ) NotFoundException.raise_if( @@ -1331,6 +1332,8 @@ def make_selection( activity_ar: ActivityAR = activity_service.repository.find_by_uid_2( activity_uid, for_update=True ) + NotFoundException.raise_if_not(activity_ar, "Activity", activity_uid) + activity_group_version, activity_subgroup_version = ( self._get_activity_group_subgroup_version_from_activity_ar( activity_ar=activity_ar, 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 999d5373..286189e5 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 @@ -514,6 +514,7 @@ def make_selection( author_id=self.author, name=selection_create_input.name, short_name=selection_create_input.short_name, + label=selection_create_input.label, code=selection_create_input.code, description=selection_create_input.description, randomization_group=selection_create_input.randomization_group, @@ -569,6 +570,7 @@ def _patch_prepare_new_study_arm( arm_uid=current_study_arm.study_selection_uid, name=current_study_arm.name, short_name=current_study_arm.short_name, + label=current_study_arm.label, code=current_study_arm.code, description=current_study_arm.description, randomization_group=current_study_arm.randomization_group, @@ -587,6 +589,7 @@ def _patch_prepare_new_study_arm( study_uid=current_study_arm.study_uid, name=request_study_arm.name, short_name=request_study_arm.short_name, + label=request_study_arm.label, code=request_study_arm.code, description=request_study_arm.description, randomization_group=request_study_arm.randomization_group, 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 676739e5..8ccfd122 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 @@ -80,6 +80,8 @@ def __init__( self.visit_repo = self._repos.study_visit_repository self.author = user().id() + if study_uid: + self.check_if_study_exists(study_uid=study_uid) self.terms_at_specific_datetime = None if terms_at_specific_date: self.terms_at_specific_datetime = datetime.datetime( @@ -298,7 +300,7 @@ def find_by_uid( def _validate_creation(self, epoch_input: StudyEpochCreateInput): ValidationException.raise_if( epoch_input.epoch_subtype not in self.study_epoch_subtypes_by_uid, - msg="Invalid value for study epoch sub type", + msg=f"Invalid value for study epoch subtype: {epoch_input.epoch_subtype}", ) epoch_subtype_name = self.study_epoch_subtypes_by_uid[ epoch_input.epoch_subtype @@ -313,7 +315,7 @@ def _validate_update(self, epoch_input: StudyEpochEditInput): ValidationException.raise_if( epoch_input.epoch_subtype is not None and epoch_input.epoch_subtype not in self.study_epoch_subtypes_by_uid, - msg="Invalid value for study epoch sub type", + msg=f"Invalid value for study epoch subtype: {epoch_input.epoch_subtype}", ) def _get_or_create_epoch_in_specific_subtype( 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 a58178d2..db4ca38c 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 @@ -162,6 +162,8 @@ "group": "Heading 4", "subGroup": "Heading 4", "activity": "Heading 4", + "library": "Heading 4", + "instance": "Heading 4", "activityRequest": "Heading 4", "activityRequestFinal": "Heading 4", "activityPlaceholder": "Heading 4", @@ -1149,7 +1151,7 @@ def get_operational_spreadsheet( style="studyVersion", ) ] - + [TableCell(span=0, style="studyVersion")] * 2 + + [TableCell(span=0, style="studyVersion")] * 4 ), TableRow( cells=[ @@ -1159,7 +1161,7 @@ def get_operational_spreadsheet( style="studyNumber", ) ] - + [TableCell(span=0, style="studyNumber")] * 2 + + [TableCell(span=0, style="studyNumber")] * 4 ), TableRow( cells=[ @@ -1174,16 +1176,20 @@ def get_operational_spreadsheet( TableCell(span=0, style="extractedBy"), TableCell(), TableCell(), + TableCell(), + TableCell(), TableCell("Epochs", style="header1"), ] ), TableRow( cells=[ TableCell("lowest visibility layer", style="header3"), + TableCell("Library", style="header3"), TableCell("SoA group", style="header3"), TableCell("Group", style="header3"), TableCell("Subgroup", style="header3"), TableCell("Activity", style="header3"), + TableCell("Instance", style="header3"), TableCell("Topic Code", style="header3"), TableCell("ADaM Param Code", style="header3"), TableCell("Visits", style="header1"), @@ -1249,12 +1255,6 @@ def get_operational_spreadsheet( ] for study_selection_activity in selection_activities: - # Do not show activity instance placeholders - if layout == SoALayout.OPERATIONAL and not getattr( - study_selection_activity, "activity_instance", None - ): - continue - rows.append(row := TableRow()) # Visibility @@ -1284,6 +1284,14 @@ def get_operational_spreadsheet( if layout == SoALayout.OPERATIONAL: row.cells.append(TableCell(visibility, style="visibility")) + # Library + row.cells.append( + TableCell( + study_selection_activity.activity.library_name or "", + style="library", + ) + ) + # SoA Group row.cells.append( TableCell( @@ -1322,6 +1330,18 @@ def get_operational_spreadsheet( ) if layout == SoALayout.OPERATIONAL: + # Instance + row.cells.append( + TableCell( + ( + study_selection_activity.activity_instance.name + if study_selection_activity.activity_instance + else "" + ), + style="instance", + ) + ) + # Topic Code row.cells.append( TableCell( @@ -1561,7 +1581,7 @@ def _get_visit_timing(visits: list[StudyVisit], visit_timing_property: str) -> s if num_visits_in_group == 1: if ( getattr(visit, visit_timing_property) is not None - and visit.visit_class != VisitClass.SPECIAL_VISIT.name + and visit.visit_class != VisitClass.SPECIAL_VISIT ): return f"{getattr(visit, visit_timing_property):d}" @@ -1600,7 +1620,7 @@ def _get_visit_window(visit: StudyVisit) -> str: visit.min_visit_window_value, visit.max_visit_window_value, ): - if visit.visit_class == VisitClass.SPECIAL_VISIT.name: + if visit.visit_class == VisitClass.SPECIAL_VISIT: return "" if visit.min_visit_window_value == visit.max_visit_window_value == 0: # visit window is zero @@ -2471,8 +2491,9 @@ def download_operational_soa_content( <-[:HAS_STUDY_ACTIVITY_INSTANCE]-(study_value) WHERE NOT (study_activity)-[:BEFORE]-() MATCH (study_activity_instance)-[:HAS_SELECTED_ACTIVITY_INSTANCE]->(activity_instance_value:ActivityInstanceValue) - WITH has_version,study_value, study_activity_schedule, study_visit, study_epoch, study_activity_instance, study_activity, activity_instance_value, - head([(study_activity)-[:HAS_SELECTED_ACTIVITY]->(activity_value:ActivityValue) | activity_value]) as activity, + MATCH (study_activity)-[:HAS_SELECTED_ACTIVITY]->(activity_value:ActivityValue)<-[:HAS_VERSION]-(:ActivityRoot)<-[:CONTAINS_CONCEPT]-(lib:Library) + WITH has_version,study_value, study_activity_schedule, study_visit, study_epoch, study_activity_instance, study_activity, activity_instance_value, lib, + head([(study_activity)-[:HAS_SELECTED_ACTIVITY]->(av:ActivityValue) | av]) as activity, head([(study_activity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(study_activity_subgroup:StudyActivitySubGroup) -[:HAS_SELECTED_ACTIVITY_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) | { @@ -2501,6 +2522,7 @@ def download_operational_soa_content( ELSE "LATEST on "+apoc.temporal.format(datetime(), 'yyyy-MM-dd HH:mm:ss zzz') END as study_version, study_value.study_number AS study_number, + lib.name AS library, study_visit.short_visit_label AS visit, epoch_name AS epoch, activity.name AS activity, 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 e8d0a699..ebb1281b 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 @@ -43,6 +43,15 @@ class StudySelectionMixin: + def check_if_study_exists(self, study_uid: str): + exceptions.NotFoundException.raise_if_not( + self._repos.study_definition_repository.study_exists_by_uid( + study_uid=study_uid + ), + "Study", + study_uid, + ) + @trace_calls def update_ctterm_maps(self, terms_at_specific_datetime: datetime | None = None): study_epoch_types = set() 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 42d8488d..11e7bbd8 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 @@ -125,6 +125,7 @@ def __init__( self.repo = self._repos.study_visit_repository self.author = user().id() + self.check_if_study_exists(study_uid=study_uid) self.terms_at_specific_datetime = ( self.get_study_standard_version_ct_terms_datetime( study_uid=study_uid, @@ -162,9 +163,7 @@ def _transform_all_to_response_history_model( # 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.update_ct_term_properties_of_study_visit(visit) - study_visit: StudyVisitBase = StudyVisitBase.transform_to_response_model( - visit, derive_props_based_on_timeline=False - ) + study_visit: StudyVisitBase = StudyVisitBase.transform_to_response_model(visit) study_visit.change_type = visit.change_type study_visit.end_date = convert_to_datetime(visit.end_date) @@ -309,6 +308,7 @@ def get_all_visits( filter_operator: FilterOperator = FilterOperator.AND, total_count: bool = False, study_value_version: str | None = None, + derive_props_based_on_timeline: bool = False, ) -> GenericFilteringReturn[StudyVisit]: StudyService.check_if_study_uid_and_version_exists( study_uid, study_value_version @@ -319,7 +319,9 @@ def get_all_visits( ) visits = [ StudyVisit.transform_to_response_model( - visit, study_value_version=study_value_version + visit, + study_value_version=study_value_version, + derive_props_based_on_timeline=derive_props_based_on_timeline, ) for visit in visits ] @@ -363,7 +365,6 @@ def get_distinct_values_for_header( # Return values for field_name return header_values - @db.transaction def get_all_references(self, study_uid: str) -> list[StudyVisit]: visits = self._get_all_visits(study_uid) result = [] @@ -377,9 +378,11 @@ def get_all_references(self, study_uid: str) -> list[StudyVisit]: return result @staticmethod - @db.transaction def find_by_uid( - study_uid: str, uid: str, study_value_version: str | None = None + study_uid: str, + uid: str, + study_value_version: str | None = None, + derive_props_based_on_timeline: bool = False, ) -> StudyVisit | None: """ finds latest version of visit by uid, status ans version @@ -400,7 +403,9 @@ def find_by_uid( if study_visit is None: raise exceptions.NotFoundException("Study Visit", uid) - return StudyVisit.transform_to_response_model(study_visit) + return StudyVisit.transform_to_response_model( + study_visit, derive_props_based_on_timeline=derive_props_based_on_timeline + ) def _chronological_order_check( self, @@ -430,28 +435,35 @@ def _chronological_order_check( def _validate_derived_properties( self, visit_vo: StudyVisitVO, ordered_visits: list[StudyVisitVO] ): - if ( - visit_vo.visit_class != VisitClass.SPECIAL_VISIT - and visit_vo.visit_subclass - not in ( - VisitSubclass.ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV, - VisitSubclass.ANCHOR_VISIT_IN_GROUP_OF_SUBV, - ) - ): + if visit_vo.visit_class != VisitClass.SPECIAL_VISIT: error_dict = {} chronological_order_dict = {} for idx, visit in enumerate(ordered_visits): if ( visit_vo.uid != visit.uid and visit.visit_class != VisitClass.SPECIAL_VISIT - and visit.visit_subclass - != VisitSubclass.ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV ): - if visit_vo.visit_number == visit.visit_number: + exclude_from_comparison = ( + ( + visit_vo.visit_subclass + == VisitSubclass.ANCHOR_VISIT_IN_GROUP_OF_SUBV + and visit.visit_subclass + == VisitSubclass.ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV + ) + or visit_vo.visit_subclass + == VisitSubclass.ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV + ) + if ( + visit_vo.visit_number == visit.visit_number + and not exclude_from_comparison + ): error_dict["visit number"] = visit_vo.visit_number if visit_vo.unique_visit_number == visit.unique_visit_number: error_dict["unique visit number"] = visit_vo.unique_visit_number - if visit_vo.visit_name == visit.visit_name: + if ( + visit_vo.visit_name == visit.visit_name + and not exclude_from_comparison + ): error_dict["visit name"] = visit_vo.visit_name if visit_vo.visit_short_name == visit.visit_short_name: error_dict["visit short name"] = visit_vo.visit_short_name @@ -781,13 +793,13 @@ def _validate_visit( ] ValidationException.raise_if( not preview - and visit_input.visit_class == VisitClass.NON_VISIT.name + and visit_input.visit_class == VisitClass.NON_VISIT and VisitClass.NON_VISIT in visits_classes, msg=f"There's already and exists Non Visit in Study {visit_vo.study_uid}", ) ValidationException.raise_if( not preview - and visit_input.visit_class == VisitClass.UNSCHEDULED_VISIT.name + and visit_input.visit_class == VisitClass.UNSCHEDULED_VISIT and VisitClass.UNSCHEDULED_VISIT in visits_classes, msg=f"There's already and exists an Unscheduled Visit in Study {visit_vo.study_uid}", ) @@ -934,6 +946,10 @@ def _from_input_values( req_time_unit_ar: UnitDefinitionAR = unit_repository.find_by_uid_2( create_input.time_unit_uid ) + if req_time_unit_ar is None: + raise exceptions.ValidationException( + msg=f"Time unit with UID '{create_input.time_unit_uid}' does not exist." + ) req_time_unit = req_time_unit_ar.concept_vo else: req_time_unit = None @@ -942,6 +958,10 @@ def _from_input_values( window_time_unit_ar: UnitDefinitionAR = unit_repository.find_by_uid_2( create_input.visit_window_unit_uid ) + if window_time_unit_ar is None: + raise exceptions.ValidationException( + msg=f"Visit window unit with UID '{create_input.visit_window_unit_uid}' does not exist." + ) window_time_unit = window_time_unit_ar.concept_vo if window_time_unit.name is None: raise exceptions.ValidationException( @@ -978,13 +998,17 @@ def _from_input_values( name="Week", conversion_factor_to_master=self._week_unit.concept_vo.conversion_factor_to_master, ) - visit_class = ( - VisitClass[create_input.visit_class] if create_input.visit_class else None + visit_contact_mode = self.study_visit_contact_modes_by_uid.get( + create_input.visit_contact_mode_uid + ) + exceptions.ValidationException.raise_if_not( + visit_contact_mode, + msg=f"Visit contact mode '{create_input.visit_contact_mode_uid}' is invalid.", ) - visit_subclass = ( - VisitSubclass[create_input.visit_subclass] - if create_input.visit_subclass - else None + visit_type = self.study_visit_types_by_uid.get(create_input.visit_type_uid) + exceptions.ValidationException.raise_if_not( + visit_type, + msg=f"Visit type with UID '{create_input.visit_type_uid}' is not valid.", ) study_visit_vo = StudyVisitVO( uid=self.repo.generate_uid(), @@ -998,17 +1022,15 @@ def _from_input_values( description=create_input.description, start_rule=create_input.start_rule, end_rule=create_input.end_rule, - visit_contact_mode=self.study_visit_contact_modes_by_uid[ - create_input.visit_contact_mode_uid - ], + visit_contact_mode=visit_contact_mode, epoch_allocation=( - self.study_visit_epoch_allocations_by_uid[ + self.study_visit_epoch_allocations_by_uid.get( create_input.epoch_allocation_uid - ] + ) if create_input.epoch_allocation_uid else None ), - visit_type=self.study_visit_types_by_uid[create_input.visit_type_uid], + visit_type=visit_type, start_date=datetime.datetime.now(datetime.timezone.utc), author_id=self.author, author_username=UserInfoService().get_author_username_from_id( @@ -1018,16 +1040,16 @@ def _from_input_values( day_unit_object=day_unit_object, week_unit_object=week_unit_object, epoch_connector=epoch, - visit_class=visit_class, - visit_subclass=visit_subclass if create_input.visit_subclass else None, + visit_class=create_input.visit_class, + visit_subclass=create_input.visit_subclass, is_global_anchor_visit=create_input.is_global_anchor_visit, is_soa_milestone=create_input.is_soa_milestone, - visit_number=self.derive_visit_number(visit_class=visit_class), - visit_order=self.derive_visit_number(visit_class=visit_class), + visit_number=self.derive_visit_number(visit_class=create_input.visit_class), + visit_order=self.derive_visit_number(visit_class=create_input.visit_class), repeating_frequency=( - self.study_visit_repeating_frequencies_by_uid[ + self.study_visit_repeating_frequencies_by_uid.get( create_input.repeating_frequency_uid - ] + ) if create_input.repeating_frequency_uid else None ), @@ -1046,13 +1068,13 @@ def _from_input_values( missing_fields.append("time_value") ValidationException.raise_if( missing_fields, - msg=f"The following fields are missing '{missing_fields}' for the Visit with Visit Class '{visit_class.value}'.", + msg=f"The following fields are missing '{missing_fields}' for the Visit with Visit Class '{study_visit_vo.visit_class.value}'.", ) study_visit_vo.timepoint = self._create_timepoint_simple_concept( study_visit_input=create_input ) - if study_visit_vo.visit_class == visit_class.MANUALLY_DEFINED_VISIT: + if study_visit_vo.visit_class == VisitClass.MANUALLY_DEFINED_VISIT: study_visit_vo.visit_number = create_input.visit_number # type: ignore[assignment] study_visit_vo.vis_unique_number = create_input.unique_visit_number study_visit_vo.vis_short_name = create_input.visit_short_name @@ -1060,7 +1082,7 @@ def _from_input_values( visit_name=create_input.visit_name ) elif ( - study_visit_vo.visit_class != visit_class.MANUALLY_DEFINED_VISIT + study_visit_vo.visit_class != VisitClass.MANUALLY_DEFINED_VISIT and any( [ create_input.visit_number, @@ -1095,6 +1117,38 @@ def synchronize_visit_numbers( self.assign_props_derived_from_visit_number(study_visit=visit) self.repo.save(visit) + def synchronize_unique_visit_numbers_for_subvisits( + self, + ordered_visits: list[StudyVisitVO], + anchor_visit: str | None, + ): + """ + Synchronizes unique visit numbers for subvisits within a visit group. + + Compares the stored unique visit number (vis_unique_number from DB) with the + derived unique visit number (calculated based on position in group) and updates + only the visits where they differ. + + :param ordered_visits: List of all ordered study visits + :param anchor_visit: UID of the anchor visit in a group of subvisits + :return: + """ + # Filter visits that belong to the specific group of subvisits + subvisits_in_group = [ + visit + for visit in ordered_visits + if visit.visit_sublabel_reference == anchor_visit + ] + + for visit in subvisits_in_group: + # Compare stored vs derived unique visit numbers and update only if different + # vis_unique_number is what's stored in DB + # unique_visit_number is the derived property based on current position + if visit.vis_unique_number != visit.unique_visit_number: + # Reassign properties that depend on the visit number + self.assign_props_derived_from_visit_number(study_visit=visit) + self.repo.save(visit) + def assign_props_derived_from_visit_number(self, study_visit: StudyVisitVO): """ Assigns some properties of StudyVisitVO that are derived from Visit Number. @@ -1107,6 +1161,8 @@ def assign_props_derived_from_visit_number(self, study_visit: StudyVisitVO): study_visit.visit_name_sc = self._create_visit_name_simple_concept( visit_name=study_visit.derive_visit_name() ) + study_visit.vis_short_name = study_visit.visit_short_name + study_visit.vis_unique_number = study_visit.unique_visit_number def assign_props_derived_from_visit_absolute_timing( self, study_visit_vo: StudyVisitVO @@ -1158,7 +1214,7 @@ def assign_props_derived_from_visit_absolute_timing( ) ) - @db.transaction + @ensure_transaction(db) def create(self, study_uid: str, study_visit_input: StudyVisitCreateInput): acquire_write_lock_study_value(uid=study_uid) study_visits = self.repo.find_all_visits_by_study_uid(study_uid) @@ -1176,17 +1232,25 @@ def create(self, study_uid: str, study_visit_input: StudyVisitCreateInput): ) self.assign_props_derived_from_visit_number(study_visit=study_visit) self.assign_props_derived_from_visit_absolute_timing(study_visit_vo=study_visit) + timeline.add_visit(study_visit) + ordered_visits = timeline.ordered_study_visits added_item = self.repo.save(study_visit, create=True) - timeline.add_visit(added_item) - - ordered_visits = timeline.ordered_study_visits # if added item is not last in ordered_study_visits, then we have to synchronize Visit Numbers if added_item.uid != ordered_visits[-1].uid: self.synchronize_visit_numbers( ordered_visits=ordered_visits, start_index_to_synchronize=int(added_item.visit_number), ) + if ( + added_item.visit_subclass + == VisitSubclass.ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV + ): + self.synchronize_unique_visit_numbers_for_subvisits( + ordered_visits=ordered_visits, + anchor_visit=added_item.visit_sublabel_reference, + ) + return StudyVisit.transform_to_response_model(added_item) def update_ct_term_properties_of_study_visit( @@ -1235,7 +1299,7 @@ def update_ct_term_properties_of_study_visit( return visit - @db.transaction + @ensure_transaction(db) def preview(self, study_uid: str, study_visit_input: StudyVisitCreateInput): study_visits = self.repo.find_all_visits_by_study_uid(study_uid) @@ -1251,9 +1315,10 @@ 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.assign_props_derived_from_visit_number(study_visit=study_visit) return StudyVisit.transform_to_response_model(study_visit) - @db.transaction + @ensure_transaction(db) def edit( self, study_uid: str, @@ -1275,50 +1340,59 @@ def edit( ) updated_visit = self._from_input_values(study_visit_input, epoch) update_dict = { - k: v - for k, v in dataclasses.asdict(updated_visit).items() - if k not in ["uid"] + field.name: getattr(updated_visit, field.name) + for field in dataclasses.fields(updated_visit) + if field.name not in ["uid", "study_visit_group"] } - update_dict["time_unit_object"] = updated_visit.time_unit_object - update_dict["window_unit_object"] = updated_visit.window_unit_object - update_dict["day_unit_object"] = updated_visit.day_unit_object - update_dict["week_unit_object"] = updated_visit.week_unit_object - update_dict["timepoint"] = updated_visit.timepoint - update_dict["study_day"] = updated_visit.study_day - update_dict["study_duration_days"] = updated_visit.study_duration_days - update_dict["study_duration_weeks"] = updated_visit.study_duration_weeks - update_dict["week_in_study"] = updated_visit.week_in_study - update_dict["study_week"] = updated_visit.study_week - update_dict["visit_name_sc"] = updated_visit.visit_name_sc - # Assigning study visit group before Edit was it can't be modified in Edit flow - update_dict["study_visit_group"] = study_visit.study_visit_group - - new_study_visit = dataclasses.replace(study_visit, **update_dict) + + # Preserve study_visit_group from original as it can't be modified in Edit flow + new_study_visit = dataclasses.replace( + study_visit, **update_dict, study_visit_group=study_visit.study_visit_group + ) new_study_visit.epoch_connector = epoch timeline.update_visit(new_study_visit) + ordered_visits = timeline.ordered_study_visits + self.assign_props_derived_from_visit_number(study_visit=new_study_visit) + self.assign_props_derived_from_visit_absolute_timing( + study_visit_vo=new_study_visit + ) self._validate_visit(study_visit_input, new_study_visit, timeline, create=False) + self.repo.save(new_study_visit) - ordered_visits = timeline.ordered_study_visits # If Visit Number was edited, then we have to synchronize the Visit Numbers in the database - if study_visit.visit_order != new_study_visit.visit_order: - if new_study_visit.visit_order < study_visit.visit_order: - start_index_to_sync = int(new_study_visit.visit_order) - 1 - else: - start_index_to_sync = int(study_visit.visit_order) - 1 + start_index_to_sync = ( + min(new_study_visit.visit_order, study_visit.visit_order) - 1 + ) + # For Information visit the visit order is 0, so we have to ensure that start index is not negative + start_index_to_sync = max(start_index_to_sync, 0) + self.synchronize_visit_numbers( ordered_visits=ordered_visits, start_index_to_synchronize=start_index_to_sync, edited_visit=new_study_visit, ) - self.assign_props_derived_from_visit_absolute_timing( - study_visit_vo=new_study_visit - ) - self.assign_props_derived_from_visit_number(study_visit=new_study_visit) - self.repo.save(new_study_visit) + # Synchronize unique visit numbers for any affected subvisit groups + anchors_to_sync = set() + if ( + study_visit.visit_subclass + == VisitSubclass.ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV + ): + anchors_to_sync.add(study_visit.visit_sublabel_reference) + if ( + new_study_visit.visit_subclass + == VisitSubclass.ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV + ): + anchors_to_sync.add(new_study_visit.visit_sublabel_reference) + + for anchor_visit in anchors_to_sync: + self.synchronize_unique_visit_numbers_for_subvisits( + ordered_visits=ordered_visits, + anchor_visit=anchor_visit, + ) return StudyVisit.transform_to_response_model(new_study_visit) @@ -1388,7 +1462,6 @@ def delete(self, study_uid: str, study_visit_uid: str): start_index_to_synchronize=int(study_visit.visit_number) - 1, ) - @db.transaction def get_consecutive_groups(self, study_uid: str) -> list[StudyVisitGroupModel]: all_visits = self.repo.find_all_visits_by_study_uid(study_uid) known_groups = set() @@ -1408,7 +1481,6 @@ def get_consecutive_groups(self, study_uid: str) -> list[StudyVisitGroupModel]: return study_visit_groups @trace_calls - @db.transaction def audit_trail( self, visit_uid: str, @@ -1445,7 +1517,6 @@ def audit_trail( return data @trace_calls - @db.transaction def audit_trail_all_visits( self, study_uid: str, @@ -1490,7 +1561,7 @@ def audit_trail_all_visits( return data - @db.transaction + @ensure_transaction(db) def remove_visit_consecutive_group( self, study_uid: str, consecutive_visit_group_uid: str ): @@ -1505,7 +1576,7 @@ def remove_visit_consecutive_group( visit.study_visit_group = None self.repo.save(visit) - @db.transaction + @ensure_transaction(db) def assign_visit_consecutive_group( self, study_uid: str, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_conditions.feature b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_conditions.feature index 8cf2ea1c..f493e600 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_conditions.feature +++ b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_conditions.feature @@ -103,7 +103,7 @@ Feature: Manage ODM Conditions in OpenStudyBuilder API Scenario: User cannot create a new ODM condition without an English description When the user sends a request to create a new ODM condition without an Egnlish description Then the response status code must be 400 - And the response must include the message "At least one description must be in English ('eng' or 'en')." + And the response must include the message "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." Test Coverage: | TestFile | TestID | | /tests/integration/api/old/test_odm_conditions_negative.py | @TestID: test_cannot_create_a_new_odm_condition_without_an_english_description | diff --git a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_forms.feature b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_forms.feature index d4ae42b4..151b35cd 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_forms.feature +++ b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_forms.feature @@ -96,7 +96,7 @@ Feature: Manage ODM Forms in OpenStudyBuilder API Scenario: User cannot create a new ODM form without an English description When the user sends a request to create a new ODM form without an English description Then the response status code must be 400 - And the response must include the message "At least one description must be in English ('eng' or 'en')." + And the response must include the message "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." Test Coverage: | TestFile | TestID | | /tests/integration/api/old/test_odm_forms_negative.py | @TestID: test_cannot_create_a_new_odm_form_without_an_english_description | diff --git a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_items.feature b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_items.feature index b3291445..224f8600 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_items.feature +++ b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_items.feature @@ -114,7 +114,7 @@ Feature: Manage ODM Items in OpenStudyBuilder API Scenario: User cannot create an ODM item without an English description When the user sends a request to create an ODM item without an English description Then the response status code must be 400 - And the response must include the message "At least one description must be in English ('eng' or 'en')." + And the response must include the message "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." Test Coverage: | TestFile | TestID | | /tests/integration/api/old/test_odm_items_negative.py | @TestID: test_cannot_create_a_new_odm_item_without_an_english_description | diff --git a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_methods.feature b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_methods.feature index 40b00b20..81438ec7 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_methods.feature +++ b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_methods.feature @@ -64,7 +64,7 @@ Feature: Manage ODM Methods in OpenStudyBuilder API Scenario: User cannot create a new ODM method without an English description When the user sends a request to create an ODM method without an English description Then the response status code must be 400 - And the response must include the message "At least one description must be in English ('eng' or 'en')." + And the response must include the message "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." Test Coverage: | TestFile | TestID | | /tests/integration/api/old/test_odm_methods_negative.py | @TestID: test_cannot_create_a_new_odm_method_without_an_english_description | diff --git a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_study_events.feature b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_study_events.feature index 15f5f83e..8e2575d8 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_study_events.feature +++ b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_study_events.feature @@ -64,7 +64,7 @@ Feature: Manage ODM Study Events in OpenStudyBuilder API Scenario: User cannot create an ODM study event without an English description When the user sends a request to create a new ODM study event without an English description Then the response status code must be 400 - And the response must include the message "At least one description must be in English ('eng' or 'en')." + And the response must include the message "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." Test Coverage: | TestFile | TestID | | /tests/integration/api/old/test_odm_study_events_negative.py | @TestID: test_cannot_create_a_new_odm_study_event_without_an_english_description | diff --git a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_vendor_attributes.feature b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_vendor_attributes.feature index 8dee1bf2..d6c47306 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_vendor_attributes.feature +++ b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/odm_vendor_attributes.feature @@ -64,7 +64,7 @@ Feature: Manage ODM Vendor Attributes in OpenStudyBuilder API Scenario: User cannot create a new ODM vendor attribute without an English description When the user sends a request to create an ODM vendor attribute without an English description Then the response status code must be 400 - And the response must include the message "At least one description must be in English ('eng' or 'en')." + And the response must include the message "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." Test Coverage: | TestFile | TestID | | /tests/integration/api/old/test_odm_vendor_attributes_negative.py | @TestID: test_cannot_create_a_new_odm_vendor_attribute_without_an_english_description | diff --git a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/omd_item_groups.feature b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/omd_item_groups.feature index f904858d..dd9a14af 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/omd_item_groups.feature +++ b/clinical-mdr-api/clinical_mdr_api/tests/acceptance/features/library/data_collection_standards/odm_crf/omd_item_groups.feature @@ -122,7 +122,7 @@ Feature: Manage ODM Item Groups in OpenStudyBuilder API Scenario: User cannot create an ODM item group without an English description When the user sends a request to create an ODM item group without an English description Then the response status code must be 400 - And the response must include the message "At least one description must be in English ('eng' or 'en')." + And the response must include the message "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." Test Coverage: | TestFile | TestID | | /tests/integration/api/old/test_odm_item_groups_negative.py | @TestID: test_cannot_create_a_new_odm_item_group_without_an_english_description | diff --git a/clinical-mdr-api/clinical_mdr_api/tests/auth/integration/routes.py b/clinical-mdr-api/clinical_mdr_api/tests/auth/integration/routes.py index 350fbdc7..584e2d1e 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/auth/integration/routes.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/auth/integration/routes.py @@ -3,6 +3,7 @@ """ ALL_ROUTES_METHODS_ROLES = ( + ("/iso/639", "GET", {"Library.Read", "Study.Read"}), ("/feature-flags/{serial_number}", "GET", {"Admin.Read"}), ("/feature-flags", "POST", {"Admin.Write"}), ("/feature-flags/{serial_number}", "PATCH", {"Admin.Write"}), @@ -24,18 +25,6 @@ ("/concepts/odms/forms/{odm_form_uid}/activations", "DELETE", {"Library.Write"}), ("/concepts/odms/forms/{odm_form_uid}/activations", "POST", {"Library.Write"}), ("/concepts/odms/forms/{odm_form_uid}/item-groups", "POST", {"Library.Write"}), - ("/concepts/odms/forms/{odm_form_uid}/vendor-elements", "POST", {"Library.Write"}), - ( - "/concepts/odms/forms/{odm_form_uid}/vendor-attributes", - "POST", - {"Library.Write"}, - ), - ( - "/concepts/odms/forms/{odm_form_uid}/vendor-element-attributes", - "POST", - {"Library.Write"}, - ), - ("/concepts/odms/forms/{odm_form_uid}/vendors", "POST", {"Library.Write"}), ("/concepts/odms/forms/{odm_form_uid}", "DELETE", {"Library.Write"}), ("/concepts/odms/forms/study-events", "GET", {"Library.Read"}), ("/concepts/odms/item-groups", "GET", {"Library.Read"}), @@ -79,26 +68,6 @@ "POST", {"Library.Write"}, ), - ( - "/concepts/odms/item-groups/{odm_item_group_uid}/vendor-elements", - "POST", - {"Library.Write"}, - ), - ( - "/concepts/odms/item-groups/{odm_item_group_uid}/vendor-attributes", - "POST", - {"Library.Write"}, - ), - ( - "/concepts/odms/item-groups/{odm_item_group_uid}/vendor-element-attributes", - "POST", - {"Library.Write"}, - ), - ( - "/concepts/odms/item-groups/{odm_item_group_uid}/vendors", - "POST", - {"Library.Write"}, - ), ("/concepts/odms/item-groups/{odm_item_group_uid}", "DELETE", {"Library.Write"}), ("/concepts/odms/items", "GET", {"Library.Read"}), ("/concepts/odms/items/headers", "GET", {"Library.Read"}), @@ -112,18 +81,6 @@ ("/concepts/odms/items/{odm_item_uid}/approvals", "POST", {"Library.Write"}), ("/concepts/odms/items/{odm_item_uid}/activations", "DELETE", {"Library.Write"}), ("/concepts/odms/items/{odm_item_uid}/activations", "POST", {"Library.Write"}), - ("/concepts/odms/items/{odm_item_uid}/vendor-elements", "POST", {"Library.Write"}), - ( - "/concepts/odms/items/{odm_item_uid}/vendor-attributes", - "POST", - {"Library.Write"}, - ), - ( - "/concepts/odms/items/{odm_item_uid}/vendor-element-attributes", - "POST", - {"Library.Write"}, - ), - ("/concepts/odms/items/{odm_item_uid}/vendors", "POST", {"Library.Write"}), ("/concepts/odms/items/{odm_item_uid}", "DELETE", {"Library.Write"}), ("/concepts/odms/conditions", "GET", {"Library.Read"}), ("/concepts/odms/conditions/headers", "GET", {"Library.Read"}), @@ -317,9 +274,10 @@ "DELETE", {"Admin.Write"}, ), - ("/concepts/odms/metadata/descriptions", "GET", {"Library.Read"}), + ("/concepts/odms/metadata/translated-texts", "GET", {"Library.Read"}), ("/concepts/odms/metadata/aliases", "GET", {"Library.Read"}), ("/concepts/odms/metadata/formal-expressions", "GET", {"Library.Read"}), + ("/concepts/odms/metadata/report", "POST", {"Library.Read"}), ("/concepts/odms/metadata/xmls/export", "POST", {"Library.Read"}), ("/concepts/odms/metadata/csvs/export", "POST", {"Library.Read"}), ("/concepts/odms/metadata/xmls/import", "POST", {"Library.Write"}), @@ -1308,6 +1266,11 @@ "PATCH", {"Library.Write"}, ), + ( + "/activity-item-classes/{activity_item_class_uid}/valid-codelist-mappings", + "PATCH", + {"Library.Write"}, + ), ("/activity-item-classes/{activity_item_class_uid}", "DELETE", {"Library.Write"}), ("/concepts/compounds", "GET", {"Library.Read"}), ("/concepts/compounds/versions", "GET", {"Library.Read"}), @@ -1761,6 +1724,7 @@ ("/studies/{study_uid}/interventions", "GET", {"Study.Read"}), ("/studies/{study_uid}/interventions.html", "GET", {"Study.Read"}), ("/studies/{study_uid}/interventions.docx", "GET", {"Study.Read"}), + ("/studies/{study_uid}/odm-forms", "GET", {"Study.Read"}), ("/study-objectives", "GET", {"Study.Read"}), ("/study-objectives/headers", "GET", {"Study.Read"}), ("/studies/{study_uid}/study-objectives", "GET", {"Study.Read"}), diff --git a/clinical-mdr-api/clinical_mdr_api/tests/data/odm_xml.py b/clinical-mdr-api/clinical_mdr_api/tests/data/odm_xml.py index 731382cd..51dda0f0 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/data/odm_xml.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/data/odm_xml.py @@ -15,52 +15,110 @@ - + - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - + - + XX:#bfffff !important; YY:#ffff96 !important; - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + + - + - name1 + Question1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - + + + name A + - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - + @@ -87,57 +145,123 @@ - + - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - test value + test value - + - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - + XX:#bfffff !important; YY:#ffff96 !important; - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + + - + - name1 + Question1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - + + + name A + - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 @@ -166,36 +290,76 @@ - + XX:#bfffff !important; YY:#ffff96 !important; - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + +` - + - name1 + Question1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - + + + name A + - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 @@ -226,15 +390,24 @@ + SDSVarName="sdsvarname1" osb:version="1.0"> - name1 + Question1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - + @@ -263,56 +436,108 @@ - + - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - test value - - + test value - + XX:#bfffff !important; YY:#ffff96 !important; - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - - + - + - name1 + Question1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + - + - - + + + name A - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 - description1 + Description1 + + Design Notes1 + + + Completion Instructions1 + + + Display Text1 + expression1 @@ -343,45 +568,47 @@ - description1 + Description1 - test value + test value - description1 + Description1 + - name1 + Question1 - description1 + Description1 + - description1 + Description1 expression1 - description1 + Description1 expression1 - description1 + Description1 expression1 @@ -414,27 +641,66 @@ - + Informed Consent and Demography form + Informed Consent and Demography formDA + + form sponsor instruction + form sponsor instructionDA + + + form instruction + form instructionDA + + + Informed Consent and Demography form + Informed Consent and Demography formDA + - + Vital signs form + Vital signs formDA + + form sponsor instruction + form sponsor instructionDA + + + form instruction + form instructionDA + + + Vital signs form + Vital signs formDA + - + VS:#bfffff !important; Blood pressure and pulse + Blood pressure and pulseDA + + item group sponsor instruction + item group sponsor instructionDA + + + item group instruction + item group instructionDA + + + Blood pressure and pulse + Blood pressure and pulseDA + @@ -443,11 +709,24 @@ - + DM:#bfffff !important; General Demographic item group + General Demographic item groupDA + + item group sponsor instruction + item group sponsor instructionDA + + + item group instruction + item group instructionDA + + + General Demographic item group + General Demographic item groupDA + @@ -460,11 +739,24 @@ - + DM:#bfffff !important; Informed Consent item group + Informed Consent item groupDA + + item group sponsor instruction + item group sponsor instructionDA + + + item group instruction + item group instructionDA + + + Informed Consent item group + Informed Consent item groupDA + @@ -473,222 +765,573 @@ - + VS:#bfffff !important; Vital signs + Vital signsDA + + item group sponsor instruction + item group sponsor instructionDA + + + item group instruction + item group instructionDA + + + Vital signs + Vital signsDA + - + Age + AgeDA Age + AgeDA - + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Age + AgeDA + + inner text - + Anatomical Location + Anatomical LocationDA Anatomical Location of the measurement + Anatomical Location of the measurementDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Anatomical Location of the measurement + Anatomical Location of the measurementDA + inner text - + Date informed consent obtained + Date informed consent obtainedDA Informed Consent DATE + Informed Consent DATEDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Informed Consent DATE + Informed Consent DATEDA + - + Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ] + Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]DA Informed Consent DATE (Legal or authorised representative 2) + Informed Consent DATE (Legal or authorised representative 2)DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Informed Consent DATE (Legal or authorised representative 2) + Informed Consent DATE (Legal or authorised representative 2)DA + - + Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ] + Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]DA Informed Consent Date (Legal or authorised representative 1) + Informed Consent Date (Legal or authorised representative 1)DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Informed Consent Date (Legal or authorised representative 1) + Informed Consent Date (Legal or authorised representative 1)DA + - + Date of birth + Date of birthDA Date of birth + Date of birthDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Date of birth + Date of birthDA + - + Date of birth (only for Argus interface) [hidden ] + Date of birth (only for Argus interface) [hidden ]DA Date of birth (only for Argus interface) [hidden ] + Date of birth (only for Argus interface) [hidden ]DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Date of birth (only for Argus interface) [hidden ] + Date of birth (only for Argus interface) [hidden ]DA + - + Date of examination + Date of examinationDA Date of examination [de-activated ] + Date of examination [de-activated ]DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Date of examination [de-activated ] + Date of examination [de-activated ]DA + - + Diastolic blood pressure + Diastolic blood pressureDA Diastolic blood pressure + Diastolic blood pressureDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Diastolic blood pressure + Diastolic blood pressureDA + - + Ethnicity + EthnicityDA Ethinicity + EthinicityDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Ethinicity + EthinicityDA + - + Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ] + Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]DA Informed Consent Time (Legal or authorised representative 1) + Informed Consent Time (Legal or authorised representative 1)DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Informed Consent Time (Legal or authorised representative 1) + Informed Consent Time (Legal or authorised representative 1)DA + - + Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ] + Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]DA Informed Consent Time (Legal or authorised representative 2) + Informed Consent Time (Legal or authorised representative 2)DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Informed Consent Time (Legal or authorised representative 2) + Informed Consent Time (Legal or authorised representative 2)DA + - + Laterality + LateralityDA Laterality of the measurement + Laterality of the measurementDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Laterality of the measurement + Laterality of the measurementDA + - + Position + PositionDA Position of the subject + Position of the subjectDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Position of the subject + Position of the subjectDA + - + Previous Subject No. + Previous Subject No.DA Previous Subject No. + Previous Subject No.DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Previous Subject No. + Previous Subject No.DA + - + Pulse + PulseDA Pulse + PulseDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Pulse + PulseDA + - + Race + RaceDA Race + RaceDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Race + RaceDA + - + Race other + Race otherDA Race other + Race otherDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Race other + Race otherDA + - + Sex [de-activated ] + Sex [de-activated ]DA Sex [de-activated ] + Sex [de-activated ]DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Sex [de-activated ] + Sex [de-activated ]DA + - + Sex [read-only ] + Sex [read-only ]DA Sex [read-only ] + Sex [read-only ]DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Sex [read-only ] + Sex [read-only ]DA + - + Study ID + Study IDDA Study Identifier + Study IdentifierDA - + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Study Identifier + Study IdentifierDA + + - + Subject No. [read-only ] + Subject No. [read-only ]DA Subject No. + Subject No.DA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Subject No. + Subject No.DA + - + Systolic blood pressure + Systolic blood pressureDA Systolic blood pressure + Systolic blood pressureDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Systolic blood pressure + Systolic blood pressureDA + - + Time informed consent obtained + Time informed consent obtainedDA Informed Consent time + Informed Consent timeDA + + item sponsor instruction + item sponsor instructionDA + + + item instruction + item instructionDA + + + Informed Consent time + Informed Consent timeDA + Condition 1 Description + Condition 1 DescriptionDA Condition 1 Description Name + Condition 1 Description NameDA Formal Expression 1 @@ -760,7 +1403,7 @@ "vendor_elements": [], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000008", + "uid": "OdmVendorAttribute_000007", "name": "connectivity", "data_type": "string", "compatible_types": ["ItemGroupRef"], @@ -785,17 +1428,17 @@ "url": "url2", "vendor_elements": [ { - "uid": "OdmVendorElement_000001", - "name": "Sometag", - "compatible_types": ["ItemDef"], + "uid": "odm_vendor_element2", + "name": "NameTwo", + "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "status": "Final", "version": "1.0", "possible_actions": ["inactivate", "new_version"], }, { - "uid": "odm_vendor_element2", - "name": "nameTwo", - "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], + "uid": "OdmVendorElement_000001", + "name": "Sometag", + "compatible_types": ["ItemDef"], "status": "Final", "version": "1.0", "possible_actions": ["inactivate", "new_version"], @@ -803,7 +1446,7 @@ ], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000006", + "uid": "OdmVendorAttribute_000005", "name": "allows", "data_type": "string", "compatible_types": ["FormDef"], @@ -812,16 +1455,7 @@ "possible_actions": ["inactivate", "new_version"], }, { - "uid": "OdmVendorAttribute_000001", - "name": "allowsMultiChoice", - "data_type": "string", - "compatible_types": ["ItemDef"], - "status": "Final", - "version": "1.0", - "possible_actions": ["inactivate", "new_version"], - }, - { - "uid": "OdmVendorAttribute_000005", + "uid": "OdmVendorAttribute_000004", "name": "dataEntryRequired", "data_type": "string", "compatible_types": ["ItemRef"], @@ -830,7 +1464,7 @@ "possible_actions": ["inactivate", "new_version"], }, { - "uid": "OdmVendorAttribute_000003", + "uid": "OdmVendorAttribute_000002", "name": "gr", "data_type": "string", "compatible_types": ["ItemGroupDef"], @@ -839,7 +1473,7 @@ "possible_actions": ["inactivate", "new_version"], }, { - "uid": "OdmVendorAttribute_000007", + "uid": "OdmVendorAttribute_000006", "name": "locked", "data_type": "string", "compatible_types": ["ItemGroupRef"], @@ -848,7 +1482,7 @@ "possible_actions": ["inactivate", "new_version"], }, { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "compatible_types": ["ItemRef"], @@ -868,7 +1502,7 @@ "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorAttribute_000006", + "uid": "OdmVendorAttribute_000005", "name": "allows", "library_name": "Sponsor", "compatible_types": ["FormDef"], @@ -886,31 +1520,6 @@ "vendor_element": None, "possible_actions": ["inactivate", "new_version"], }, - { - "start_date": "2022-12-01T13:06:41.198850+00:00", - "end_date": None, - "status": "Final", - "version": "1.0", - "author_username": "unknown-user@example.com", - "change_description": "Approved version", - "uid": "OdmVendorAttribute_000001", - "name": "allowsMultiChoice", - "library_name": "Sponsor", - "compatible_types": ["ItemDef"], - "data_type": "string", - "value_regex": None, - "vendor_namespace": { - "uid": "odm_vendor_namespace2", - "name": "OSB", - "prefix": "osb", - "url": "url2", - "status": "Final", - "version": "1.0", - "possible_actions": ["inactivate", "new_version"], - }, - "vendor_element": None, - "possible_actions": ["inactivate", "new_version"], - }, { "start_date": "2022-12-08T07:29:45.187324+00:00", "end_date": None, @@ -918,7 +1527,7 @@ "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorAttribute_000008", + "uid": "OdmVendorAttribute_000007", "name": "connectivity", "library_name": "Sponsor", "compatible_types": ["ItemGroupRef"], @@ -943,7 +1552,7 @@ "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorAttribute_000005", + "uid": "OdmVendorAttribute_000004", "name": "dataEntryRequired", "library_name": "Sponsor", "compatible_types": ["ItemRef"], @@ -968,7 +1577,7 @@ "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorAttribute_000003", + "uid": "OdmVendorAttribute_000002", "name": "gr", "library_name": "Sponsor", "compatible_types": ["ItemGroupDef"], @@ -993,7 +1602,7 @@ "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorAttribute_000007", + "uid": "OdmVendorAttribute_000006", "name": "locked", "library_name": "Sponsor", "compatible_types": ["ItemGroupRef"], @@ -1018,7 +1627,7 @@ "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "library_name": "Sponsor", "compatible_types": ["ItemRef"], @@ -1043,7 +1652,7 @@ "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorAttribute_000002", + "uid": "OdmVendorAttribute_000001", "name": "someAttr", "library_name": "Sponsor", "compatible_types": [], @@ -1063,14 +1672,14 @@ ], "vendor_elements": [ { - "start_date": "2022-12-01T13:06:41.819393+00:00", + "start_date": "2022-12-12T09:16:09.313000+00:00", "end_date": None, "status": "Final", "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "OdmVendorElement_000001", - "name": "Sometag", + "uid": "odm_vendor_element2", + "name": "NameTwo", "library_name": "Sponsor", "vendor_namespace": { "uid": "odm_vendor_namespace2", @@ -1081,29 +1690,19 @@ "version": "1.0", "possible_actions": ["inactivate", "new_version"], }, - "vendor_attributes": [ - { - "uid": "OdmVendorAttribute_000002", - "name": "someAttr", - "data_type": "string", - "compatible_types": [], - "status": "Final", - "version": "1.0", - "possible_actions": ["inactivate", "new_version"], - } - ], - "compatible_types": ["ItemDef"], + "vendor_attributes": [], + "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "possible_actions": ["inactivate", "new_version"], }, { - "start_date": "2022-12-12T09:16:09.313000+00:00", + "start_date": "2022-12-01T13:06:41.819393+00:00", "end_date": None, "status": "Final", "version": "1.0", "author_username": "unknown-user@example.com", "change_description": "Approved version", - "uid": "odm_vendor_element2", - "name": "nameTwo", + "uid": "OdmVendorElement_000001", + "name": "Sometag", "library_name": "Sponsor", "vendor_namespace": { "uid": "odm_vendor_namespace2", @@ -1114,8 +1713,18 @@ "version": "1.0", "possible_actions": ["inactivate", "new_version"], }, - "vendor_attributes": [], - "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], + "vendor_attributes": [ + { + "uid": "OdmVendorAttribute_000001", + "name": "someAttr", + "data_type": "string", + "compatible_types": [], + "status": "Final", + "version": "1.0", + "possible_actions": ["inactivate", "new_version"], + } + ], + "compatible_types": ["ItemDef"], "possible_actions": ["inactivate", "new_version"], }, ], @@ -1138,6 +1747,7 @@ "forms": [ { "uid": "OdmForm_000001", + "oid": "F.DM", "name": "Informed Consent and Demography", "version": "1.0", "order_number": 999999, @@ -1147,6 +1757,7 @@ }, { "uid": "OdmForm_000002", + "oid": "F.VS", "name": "Vital Signs", "version": "1.0", "order_number": 999999, @@ -1172,14 +1783,47 @@ "oid": "F.DM", "repeating": "No", "sdtm_version": "", - "descriptions": [ + "translated_texts": [ { - "name": "Informed Consent and Demography form", + "text_type": "Description", "language": "en", - "description": "Informed Consent and Demography form", - "instruction": "form instruction", - "sponsor_instruction": "form sponsor instruction", - } + "text": "Informed Consent and Demography form", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent and Demography formDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "form instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "form instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "form sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "form sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent and Demography form", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent and Demography formDA", + }, ], "aliases": [{"context": "context1", "name": "name1"}], "item_groups": [ @@ -1194,7 +1838,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000007", + "uid": "OdmVendorAttribute_000006", "name": "locked", "data_type": "string", "value_regex": None, @@ -1202,7 +1846,7 @@ "vendor_namespace_uid": "odm_vendor_namespace2", }, { - "uid": "OdmVendorAttribute_000008", + "uid": "OdmVendorAttribute_000007", "name": "connectivity", "data_type": "string", "value_regex": None, @@ -1223,7 +1867,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000007", + "uid": "OdmVendorAttribute_000006", "name": "locked", "data_type": "string", "value_regex": None, @@ -1231,7 +1875,7 @@ "vendor_namespace_uid": "odm_vendor_namespace2", }, { - "uid": "OdmVendorAttribute_000008", + "uid": "OdmVendorAttribute_000007", "name": "connectivity", "data_type": "string", "value_regex": None, @@ -1245,7 +1889,7 @@ "vendor_elements": [], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000006", + "uid": "OdmVendorAttribute_000005", "name": "allows", "data_type": "string", "value_regex": None, @@ -1269,14 +1913,47 @@ "oid": "F.VS", "repeating": "No", "sdtm_version": "", - "descriptions": [ + "translated_texts": [ { - "name": "Vital signs form", + "text_type": "Description", "language": "en", - "description": "Vital signs form", - "instruction": "form instruction", - "sponsor_instruction": "form sponsor instruction", - } + "text": "Vital signs form", + }, + { + "text_type": "Description", + "language": "da", + "text": "Vital signs formDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "form instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "form instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "form sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "form sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Vital signs form", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Vital signs formDA", + }, ], "aliases": [{"context": "context1", "name": "name1"}], "item_groups": [ @@ -1291,7 +1968,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000007", + "uid": "OdmVendorAttribute_000006", "name": "locked", "data_type": "string", "value_regex": None, @@ -1312,7 +1989,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000007", + "uid": "OdmVendorAttribute_000006", "name": "locked", "data_type": "string", "value_regex": None, @@ -1326,7 +2003,7 @@ "vendor_elements": [], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000006", + "uid": "OdmVendorAttribute_000005", "name": "allows", "data_type": "string", "value_regex": None, @@ -1356,14 +2033,47 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Blood pressure and pulse", + "text_type": "Description", "language": "en", - "description": "Blood pressure and pulse", - "instruction": "item group instruction", - "sponsor_instruction": "item group sponsor instruction", - } + "text": "Blood pressure and pulse", + }, + { + "text_type": "Description", + "language": "da", + "text": "Blood pressure and pulseDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item group instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item group instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item group sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item group sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Blood pressure and pulse", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Blood pressure and pulseDA", + }, ], "aliases": [{"context": "context1", "name": "name1"}], "sdtm_domains": [ @@ -1409,7 +2119,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1435,7 +2145,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1443,7 +2153,7 @@ "vendor_namespace_uid": "odm_vendor_namespace2", }, { - "uid": "OdmVendorAttribute_000005", + "uid": "OdmVendorAttribute_000004", "name": "dataEntryRequired", "data_type": "string", "value_regex": None, @@ -1469,7 +2179,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1495,7 +2205,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1521,7 +2231,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1547,7 +2257,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1561,7 +2271,7 @@ "vendor_elements": [], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000003", + "uid": "OdmVendorAttribute_000002", "name": "gr", "data_type": "string", "value_regex": None, @@ -1589,28 +2299,61 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "General Demographic item group", + "text_type": "Description", "language": "en", - "description": "General Demographic item group", - "instruction": "item group instruction", - "sponsor_instruction": "item group sponsor instruction", - } - ], - "aliases": [{"context": "context2", "name": "name2"}], - "sdtm_domains": [ + "text": "General Demographic item group", + }, { - "term_uid": "term_root_final", - "submission_value": "submission_value_1", - "preferred_term": "preferred_term1", - "term_name": "term_value_name1", - "order": 1, - "queried_effective_date": None, - "codelist_uid": "domain_cl", - "codelist_submission_value": "DOMAIN", - "codelist_name": "SDTM Domain Abbreviation", - "date_conflict": False, + "text_type": "Description", + "language": "da", + "text": "General Demographic item groupDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item group instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item group instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item group sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item group sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "General Demographic item group", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "General Demographic item groupDA", + }, + ], + "aliases": [{"context": "context2", "name": "name2"}], + "sdtm_domains": [ + { + "term_uid": "term_root_final", + "submission_value": "submission_value_1", + "preferred_term": "preferred_term1", + "term_name": "term_value_name1", + "order": 1, + "queried_effective_date": None, + "codelist_uid": "domain_cl", + "codelist_submission_value": "DOMAIN", + "codelist_name": "SDTM Domain Abbreviation", + "date_conflict": False, } ], "items": [ @@ -1630,7 +2373,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1656,7 +2399,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1682,7 +2425,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1708,7 +2451,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1734,7 +2477,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1760,7 +2503,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1786,7 +2529,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1812,7 +2555,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1838,7 +2581,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1864,7 +2607,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1878,7 +2621,7 @@ "vendor_elements": [], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000003", + "uid": "OdmVendorAttribute_000002", "name": "gr", "data_type": "string", "value_regex": None, @@ -1906,14 +2649,47 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Informed Consent item group", + "text_type": "Description", "language": "en", - "description": "Informed Consent item group", - "instruction": "item group instruction", - "sponsor_instruction": "item group sponsor instruction", - } + "text": "Informed Consent item group", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent item groupDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item group instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item group instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item group sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item group sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent item group", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent item groupDA", + }, ], "aliases": [], "sdtm_domains": [ @@ -1947,7 +2723,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1973,7 +2749,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -1999,7 +2775,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2025,7 +2801,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2051,7 +2827,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2077,7 +2853,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2103,7 +2879,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2117,7 +2893,7 @@ "vendor_elements": [], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000003", + "uid": "OdmVendorAttribute_000002", "name": "gr", "data_type": "string", "value_regex": None, @@ -2145,14 +2921,47 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Vital signs", + "text_type": "Description", "language": "en", - "description": "Vital signs", - "instruction": "item group instruction", - "sponsor_instruction": "item group sponsor instruction", - } + "text": "Vital signs", + }, + { + "text_type": "Description", + "language": "da", + "text": "Vital signsDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item group instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item group instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item group sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item group sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Vital signs", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Vital signsDA", + }, ], "aliases": [], "sdtm_domains": [], @@ -2173,7 +2982,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2199,7 +3008,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2225,7 +3034,7 @@ "vendor": { "attributes": [ { - "uid": "OdmVendorAttribute_000004", + "uid": "OdmVendorAttribute_000003", "name": "sdv", "data_type": "string", "value_regex": None, @@ -2239,7 +3048,7 @@ "vendor_elements": [], "vendor_attributes": [ { - "uid": "OdmVendorAttribute_000003", + "uid": "OdmVendorAttribute_000002", "name": "gr", "data_type": "string", "value_regex": None, @@ -2271,20 +3080,64 @@ "sds_var_name": "AGE", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Age", + "text_type": "Description", "language": "en", - "description": "Age", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Age", + }, + { + "text_type": "Description", + "language": "da", + "text": "AgeDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Age", + }, + { + "text_type": "Question", + "language": "da", + "text": "AgeDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Age", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "AgeDA", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -2298,8 +3151,10 @@ "codelist": { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, }, "terms": [ { @@ -2320,19 +3175,10 @@ "value": "inner text", } ], - "vendor_attributes": [ - { - "uid": "OdmVendorAttribute_000001", - "name": "allowsMultiChoice", - "data_type": "string", - "value_regex": None, - "value": "true", - "vendor_namespace_uid": "odm_vendor_namespace2", - } - ], + "vendor_attributes": [], "vendor_element_attributes": [ { - "uid": "OdmVendorAttribute_000002", + "uid": "OdmVendorAttribute_000001", "name": "someAttr", "data_type": "string", "value_regex": None, @@ -2361,14 +3207,57 @@ "sds_var_name": "VSLOC where VSTESTCD=SYSBP | VSLOC where VSTESTCD=DIABP", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Anatomical Location", + "text_type": "Description", "language": "en", - "description": "Anatomical Location of the measurement", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Anatomical Location of the measurement", + }, + { + "text_type": "Description", + "language": "da", + "text": "Anatomical Location of the measurementDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Anatomical Location", + }, + { + "text_type": "Question", + "language": "da", + "text": "Anatomical LocationDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Anatomical Location of the measurement", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Anatomical Location of the measurementDA", + }, ], "aliases": [], "unit_definitions": [], @@ -2382,19 +3271,10 @@ "value": "inner text", } ], - "vendor_attributes": [ - { - "uid": "OdmVendorAttribute_000001", - "name": "allowsMultiChoice", - "data_type": "string", - "value_regex": None, - "value": "false", - "vendor_namespace_uid": "odm_vendor_namespace2", - } - ], + "vendor_attributes": [], "vendor_element_attributes": [ { - "uid": "OdmVendorAttribute_000002", + "uid": "OdmVendorAttribute_000001", "name": "someAttr", "data_type": "string", "value_regex": None, @@ -2423,14 +3303,57 @@ "sds_var_name": "RFICDTC, DSSTDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date informed consent obtained", + "text_type": "Description", "language": "en", - "description": "Informed Consent DATE", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Informed Consent DATE", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent DATEDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Date informed consent obtained", + }, + { + "text_type": "Question", + "language": "da", + "text": "Date informed consent obtainedDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent DATE", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent DATEDA", + }, ], "aliases": [], "unit_definitions": [], @@ -2461,14 +3384,57 @@ "sds_var_name": "RFICDTC, DSSTDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]", + "text_type": "Description", "language": "en", - "description": "Informed Consent DATE (Legal or authorised representative 2)", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Informed Consent DATE (Legal or authorised representative 2)", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent DATE (Legal or authorised representative 2)DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent DATE (Legal or authorised representative 2)", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent DATE (Legal or authorised representative 2)DA", + }, ], "aliases": [], "unit_definitions": [], @@ -2499,21 +3465,64 @@ "sds_var_name": "RFICDTC, DSSTDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]", + "text_type": "Description", "language": "en", - "description": "Informed Consent Date (Legal or authorised representative 1)", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } - ], - "aliases": [], - "unit_definitions": [], - "codelist": None, - "terms": [], - "activity_instances": [], - "vendor_elements": [], + "text": "Informed Consent Date (Legal or authorised representative 1)", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent Date (Legal or authorised representative 1)DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent Date (Legal or authorised representative 1)", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent Date (Legal or authorised representative 1)DA", + }, + ], + "aliases": [], + "unit_definitions": [], + "codelist": None, + "terms": [], + "activity_instances": [], + "vendor_elements": [], "vendor_attributes": [], "vendor_element_attributes": [], "possible_actions": ["inactivate", "new_version"], @@ -2537,14 +3546,57 @@ "sds_var_name": "BRTHDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date of birth", + "text_type": "Description", "language": "en", - "description": "Date of birth", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Date of birth", + }, + { + "text_type": "Description", + "language": "da", + "text": "Date of birthDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Date of birth", + }, + { + "text_type": "Question", + "language": "da", + "text": "Date of birthDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Date of birth", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Date of birthDA", + }, ], "aliases": [], "unit_definitions": [], @@ -2575,22 +3627,67 @@ "sds_var_name": "BRTHDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date of birth (only for Argus interface) [hidden ]", + "text_type": "Description", "language": "en", - "description": "Date of birth (only for Argus interface) [hidden ]", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Date of birth (only for Argus interface) [hidden ]", + }, + { + "text_type": "Description", + "language": "da", + "text": "Date of birth (only for Argus interface) [hidden ]DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Date of birth (only for Argus interface) [hidden ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Date of birth (only for Argus interface) [hidden ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Date of birth (only for Argus interface) [hidden ]", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Date of birth (only for Argus interface) [hidden ]DA", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000001", "name": "cnew codelist created by odm xml import", + "version": "1.0", "submission_value": "cnew codelist created by odm xml import", "preferred_term": "new codelist created by odm xml import", + "allows_multi_choice": False, }, "terms": [ { @@ -2628,14 +3725,57 @@ "sds_var_name": "VSDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date of examination", + "text_type": "Description", "language": "en", - "description": "Date of examination [de-activated ]", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Date of examination [de-activated ]", + }, + { + "text_type": "Description", + "language": "da", + "text": "Date of examination [de-activated ]DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Date of examination", + }, + { + "text_type": "Question", + "language": "da", + "text": "Date of examinationDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Date of examination [de-activated ]", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Date of examination [de-activated ]DA", + }, ], "aliases": [], "unit_definitions": [], @@ -2666,20 +3806,64 @@ "sds_var_name": "VSORRES where VSTESTCD=DIABP, VSORRESU where VSTESTCD=DIABP", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Diastolic blood pressure", + "text_type": "Description", "language": "en", - "description": "Diastolic blood pressure", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Diastolic blood pressure", + }, + { + "text_type": "Description", + "language": "da", + "text": "Diastolic blood pressureDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Diastolic blood pressure", + }, + { + "text_type": "Question", + "language": "da", + "text": "Diastolic blood pressureDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Diastolic blood pressure", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Diastolic blood pressureDA", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -2717,14 +3901,57 @@ "sds_var_name": "ETHNIC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Ethnicity", + "text_type": "Description", "language": "en", - "description": "Ethinicity", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Ethinicity", + }, + { + "text_type": "Description", + "language": "da", + "text": "EthinicityDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Ethnicity", + }, + { + "text_type": "Question", + "language": "da", + "text": "EthnicityDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Ethinicity", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "EthinicityDA", + }, ], "aliases": [], "unit_definitions": [], @@ -2755,14 +3982,57 @@ "sds_var_name": "RFICDTC, DSSTDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]", + "text_type": "Description", "language": "en", - "description": "Informed Consent Time (Legal or authorised representative 1)", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Informed Consent Time (Legal or authorised representative 1)", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent Time (Legal or authorised representative 1)DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent Time (Legal or authorised representative 1)", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent Time (Legal or authorised representative 1)DA", + }, ], "aliases": [], "unit_definitions": [], @@ -2793,14 +4063,57 @@ "sds_var_name": "RFICDTC, DSSTDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]", + "text_type": "Description", "language": "en", - "description": "Informed Consent Time (Legal or authorised representative 2)", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Informed Consent Time (Legal or authorised representative 2)", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent Time (Legal or authorised representative 2)DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent Time (Legal or authorised representative 2)", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent Time (Legal or authorised representative 2)DA", + }, ], "aliases": [], "unit_definitions": [], @@ -2831,14 +4144,57 @@ "sds_var_name": "VSLAT where VSTESTCD=SYSBP | VSLAT where VSTESTCD=DIABP", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Laterality", + "text_type": "Description", "language": "en", - "description": "Laterality of the measurement", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Laterality of the measurement", + }, + { + "text_type": "Description", + "language": "da", + "text": "Laterality of the measurementDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Laterality", + }, + { + "text_type": "Question", + "language": "da", + "text": "LateralityDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Laterality of the measurement", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Laterality of the measurementDA", + }, ], "aliases": [], "unit_definitions": [], @@ -2869,14 +4225,57 @@ "sds_var_name": "VSPOS where VSTESTCD=SYSBP | VSPOS where VSTESTCD=DIABP", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "en", + "text": "Position of the subject", + }, + { + "text_type": "Description", + "language": "da", + "text": "Position of the subjectDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Position", + }, + { + "text_type": "Question", + "language": "da", + "text": "PositionDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, { - "name": "Position", + "text_type": "osb:DisplayText", "language": "en", - "description": "Position of the subject", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Position of the subject", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Position of the subjectDA", + }, ], "aliases": [], "unit_definitions": [], @@ -2907,14 +4306,57 @@ "sds_var_name": "PREVSUBJ", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Previous Subject No.", + "text_type": "Description", "language": "en", - "description": "Previous Subject No.", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Previous Subject No.", + }, + { + "text_type": "Description", + "language": "da", + "text": "Previous Subject No.DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Previous Subject No.", + }, + { + "text_type": "Question", + "language": "da", + "text": "Previous Subject No.DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Previous Subject No.", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Previous Subject No.DA", + }, ], "aliases": [], "unit_definitions": [], @@ -2945,20 +4387,64 @@ "sds_var_name": "VSORRES/VSORRESU when VSTESTCD=PULSE", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Pulse", + "text_type": "Description", "language": "en", - "description": "Pulse", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Pulse", + }, + { + "text_type": "Description", + "language": "da", + "text": "PulseDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Pulse", + }, + { + "text_type": "Question", + "language": "da", + "text": "PulseDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Pulse", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "PulseDA", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -2996,14 +4482,57 @@ "sds_var_name": "RACE", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Race", + "text_type": "Description", "language": "en", - "description": "Race", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Race", + }, + { + "text_type": "Description", + "language": "da", + "text": "RaceDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Race", + }, + { + "text_type": "Question", + "language": "da", + "text": "RaceDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Race", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "RaceDA", + }, ], "aliases": [], "unit_definitions": [], @@ -3034,14 +4563,57 @@ "sds_var_name": "RACEOTH in SUPPDM", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Race other", + "text_type": "Description", "language": "en", - "description": "Race other", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Race other", + }, + { + "text_type": "Description", + "language": "da", + "text": "Race otherDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Race other", + }, + { + "text_type": "Question", + "language": "da", + "text": "Race otherDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Race other", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Race otherDA", + }, ], "aliases": [], "unit_definitions": [], @@ -3072,14 +4644,57 @@ "sds_var_name": "SEX", "origin": "Protocol Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Sex [de-activated ]", + "text_type": "Description", "language": "en", - "description": "Sex [de-activated ]", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Sex [de-activated ]", + }, + { + "text_type": "Description", + "language": "da", + "text": "Sex [de-activated ]DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Sex [de-activated ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Sex [de-activated ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Sex [de-activated ]", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Sex [de-activated ]DA", + }, ], "aliases": [], "unit_definitions": [], @@ -3110,14 +4725,57 @@ "sds_var_name": "SEX", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Sex [read-only ]", + "text_type": "Description", "language": "en", - "description": "Sex [read-only ]", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Sex [read-only ]", + }, + { + "text_type": "Description", + "language": "da", + "text": "Sex [read-only ]DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Sex [read-only ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Sex [read-only ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Sex [read-only ]", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Sex [read-only ]DA", + }, ], "aliases": [], "unit_definitions": [], @@ -3148,22 +4806,67 @@ "sds_var_name": "STUDYID", "origin": "Protocol Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Study ID", + "text_type": "Description", "language": "en", - "description": "Study Identifier", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Study Identifier", + }, + { + "text_type": "Description", + "language": "da", + "text": "Study IdentifierDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Study ID", + }, + { + "text_type": "Question", + "language": "da", + "text": "Study IDDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Study Identifier", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Study IdentifierDA", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": False, }, "terms": [ { @@ -3201,14 +4904,57 @@ "sds_var_name": "SUBJID", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Subject No. [read-only ]", + "text_type": "Description", "language": "en", - "description": "Subject No.", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Subject No.", + }, + { + "text_type": "Description", + "language": "da", + "text": "Subject No.DA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Subject No. [read-only ]", + }, + { + "text_type": "Question", + "language": "da", + "text": "Subject No. [read-only ]DA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Subject No.", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Subject No.DA", + }, ], "aliases": [], "unit_definitions": [], @@ -3239,20 +4985,64 @@ "sds_var_name": "VSORRES where VSTESTCD=SYSBP, VSORRESU where VSTESTCD=SYSBP", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "en", + "text": "Systolic blood pressure", + }, + { + "text_type": "Description", + "language": "da", + "text": "Systolic blood pressureDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Systolic blood pressure", + }, + { + "text_type": "Question", + "language": "da", + "text": "Systolic blood pressureDA", + }, { - "name": "Systolic blood pressure", + "text_type": "osb:CompletionInstructions", "language": "en", - "description": "Systolic blood pressure", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Systolic blood pressure", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Systolic blood pressureDA", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -3290,14 +5080,57 @@ "sds_var_name": "RFICDTC, DSSTDTC", "origin": "Collected Value", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Time informed consent obtained", + "text_type": "Description", "language": "en", - "description": "Informed Consent time", - "instruction": "item instruction", - "sponsor_instruction": "item sponsor instruction", - } + "text": "Informed Consent time", + }, + { + "text_type": "Description", + "language": "da", + "text": "Informed Consent timeDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Time informed consent obtained", + }, + { + "text_type": "Question", + "language": "da", + "text": "Time informed consent obtainedDA", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "item instruction", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "da", + "text": "item instructionDA", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "item sponsor instruction", + }, + { + "text_type": "osb:DesignNotes", + "language": "da", + "text": "item sponsor instructionDA", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "Informed Consent time", + }, + { + "text_type": "osb:DisplayText", + "language": "da", + "text": "Informed Consent timeDA", + }, ], "aliases": [], "unit_definitions": [], @@ -3325,14 +5158,27 @@ "formal_expressions": [ {"context": "XPath", "expression": "Formal Expression 1"} ], - "descriptions": [ + "translated_texts": [ { - "name": "Condition 1 Description Name", + "text_type": "Description", "language": "en", - "description": "Condition 1 Description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Condition 1 Description", + }, + { + "text_type": "Description", + "language": "da", + "text": "Condition 1 DescriptionDA", + }, + { + "text_type": "Question", + "language": "en", + "text": "Condition 1 Description Name", + }, + { + "text_type": "Question", + "language": "da", + "text": "Condition 1 Description NameDA", + }, ], "aliases": [], "possible_actions": ["inactivate", "new_version"], @@ -3351,27 +5197,31 @@ "formal_expressions": [ {"context": "XPath", "expression": "Formal Expression 2"} ], - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "en", + "text": "Condition 2 Description", + }, { - "name": "Condition 2 Description Name", + "text_type": "Description", + "language": "dan", + "text": "Condition 3 Description", + }, + { + "text_type": "Question", "language": "en", - "description": "Condition 2 Description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", + "text": "Condition 2 Description Name", }, { - "name": "Condition 3 Description Name", + "text_type": "Question", "language": "dan", - "description": "Condition 3 Description", - "instruction": None, - "sponsor_instruction": None, + "text": "Condition 3 Description Name", }, { - "name": "Condition 4 Description Name", + "text_type": "Question", "language": "ar", - "description": "Please update this description", - "instruction": None, - "sponsor_instruction": None, + "text": "Condition 4 Description Name", }, ], "aliases": [], @@ -3394,27 +5244,31 @@ "formal_expressions": [ {"context": "XPath", "expression": "Formal Expression 1"} ], - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "en", + "text": "Method 1 Description", + }, + { + "text_type": "Description", + "language": "da", + "text": "Method 2 Description", + }, { - "name": "Method 1 Description Name", + "text_type": "Question", "language": "ar", - "description": "Please update this description", - "instruction": None, - "sponsor_instruction": None, + "text": "Method 1 Description Name", }, { - "name": "Method 2 Description Name", + "text_type": "Question", "language": "en", - "description": "Method 1 Description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", + "text": "Method 2 Description Name", }, { - "name": "Method 3 Description Name", + "text_type": "Question", "language": "da", - "description": "Method 2 Description", - "instruction": None, - "sponsor_instruction": None, + "text": "Method 3 Description Name", }, ], "aliases": [], @@ -3436,7 +5290,7 @@ "template_parameter": False, "library_name": "Sponsor", "possible_actions": ["new_version"], - "ordinal": False, + "is_ordinal": False, "paired_codes_codelist_uid": None, "paired_names_codelist_uid": None, } @@ -3449,6 +5303,7 @@ { "codelist_uid": "CTCodelist_000001", "order": None, + "ordinal": None, "submission_value": "codelistitem codedvalue", "library_name": "Sponsor", "codelist_name": "cnew codelist created by odm xml import", @@ -3476,10 +5331,16 @@ - + description1 + + sponsor_instruction1 + + + instruction1 + test value @@ -3504,7 +5365,7 @@ "vendor_elements": [ { "uid": "OdmVendorElement_000001", - "name": "nameOne", + "name": "NameOne", "compatible_types": ["FormDef"], "status": "Final", "version": "1.0", @@ -3529,7 +5390,7 @@ "vendor_elements": [ { "uid": "odm_vendor_element2", - "name": "nameTwo", + "name": "NameTwo", "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "status": "Final", "version": "1.0", @@ -3586,7 +5447,7 @@ "author_username": "unknown-user@example.com", "change_description": "Approved version", "uid": "OdmVendorElement_000001", - "name": "nameOne", + "name": "NameOne", "library_name": "Sponsor", "vendor_namespace": { "uid": "OdmVendorNamespace_000001", @@ -3609,7 +5470,7 @@ "author_username": "unknown-user@example.com", "change_description": "Approved version", "uid": "odm_vendor_element2", - "name": "nameTwo", + "name": "NameTwo", "library_name": "Sponsor", "vendor_namespace": { "uid": "odm_vendor_namespace2", @@ -3644,6 +5505,7 @@ "forms": [ { "uid": "OdmForm_000001", + "oid": "oid1", "name": "name1", "version": "1.0", "order_number": 999999, @@ -3669,21 +5531,29 @@ "oid": "oid1", "repeating": "Yes", "sdtm_version": "", - "descriptions": [ + "translated_texts": [ { - "name": "description1", + "text_type": "Description", "language": "en", - "description": "description1", - "instruction": "instruction1", - "sponsor_instruction": "sponsor_instruction1", - } + "text": "description1", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "instruction1", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "sponsor_instruction1", + }, ], "aliases": [], "item_groups": [], "vendor_elements": [ { "uid": "OdmVendorElement_000001", - "name": "nameOne", + "name": "NameOne", "value": "test value", } ], @@ -4642,7 +6512,7 @@ }, { "uid": "odm_vendor_element1", - "name": "nameOne", + "name": "NameOne", "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "status": "Final", "version": "1.0", @@ -4650,7 +6520,7 @@ }, { "uid": "odm_vendor_element3", - "name": "nameThree", + "name": "NameThree", "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "status": "Final", "version": "1.0", @@ -4960,7 +6830,7 @@ "author_username": "unknown-user@example.com", "change_description": "Approved version", "uid": "odm_vendor_element1", - "name": "nameOne", + "name": "NameOne", "library_name": "Sponsor", "vendor_namespace": { "uid": "odm_vendor_namespace1", @@ -5001,7 +6871,7 @@ "author_username": "unknown-user@example.com", "change_description": "Approved version", "uid": "odm_vendor_element3", - "name": "nameThree", + "name": "NameThree", "library_name": "Sponsor", "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "vendor_namespace": { @@ -5046,6 +6916,7 @@ "forms": [ { "uid": "OdmForm_000005", + "oid": "F.38", "name": "Administration of 1", "version": "1.0", "order_number": 999999, @@ -5055,6 +6926,7 @@ }, { "uid": "OdmForm_000003", + "oid": "F.52", "name": "Body Measurements (with BMI) 1", "version": "1.0", "order_number": 999999, @@ -5064,6 +6936,7 @@ }, { "uid": "OdmForm_000002", + "oid": "F.98", "name": "Demography 1", "version": "1.0", "order_number": 999999, @@ -5073,6 +6946,7 @@ }, { "uid": "OdmForm_000001", + "oid": "F.47", "name": "Informed Consent 1", "version": "1.0", "order_number": 999999, @@ -5082,6 +6956,7 @@ }, { "uid": "OdmForm_000004", + "oid": "F.37", "name": "Vital Signs (Single Measurement) 1", "version": "1.0", "order_number": 999999, @@ -5107,7 +6982,7 @@ "oid": "F.38", "repeating": "Yes", "sdtm_version": "", - "descriptions": [], + "translated_texts": [], "aliases": [], "item_groups": [ { @@ -5169,14 +7044,12 @@ "oid": "F.52", "repeating": "Yes", "sdtm_version": "", - "descriptions": [ + "translated_texts": [ { - "name": "Body Measurements form including weight, height and BMI calculation. Expected use for screening visit.", + "text_type": "Description", "language": "en", - "description": "Body Measurements form including weight, height and BMI calculation. Expected use for screening visit.", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Body Measurements form including weight, height and BMI calculation. Expected use for screening visit.", + }, ], "aliases": [], "item_groups": [ @@ -5209,7 +7082,7 @@ "oid": "F.98", "repeating": "Yes", "sdtm_version": "", - "descriptions": [], + "translated_texts": [], "aliases": [], "item_groups": [ { @@ -5241,7 +7114,7 @@ "oid": "F.47", "repeating": "No", "sdtm_version": "", - "descriptions": [], + "translated_texts": [], "aliases": [], "item_groups": [ { @@ -5308,7 +7181,7 @@ "oid": "F.37", "repeating": "Yes", "sdtm_version": "", - "descriptions": [], + "translated_texts": [], "aliases": [], "item_groups": [ { @@ -5346,14 +7219,12 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Exposure as Collected", + "text_type": "Description", "language": "en", - "description": "Exposure as Collected", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Exposure as Collected", + }, ], "aliases": [], "sdtm_domains": [], @@ -5636,14 +7507,12 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Body Measurements", + "text_type": "Description", "language": "en", - "description": "Body Measurements", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Body Measurements", + }, ], "aliases": [], "sdtm_domains": [], @@ -5761,14 +7630,12 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Demographics", + "text_type": "Description", "language": "en", - "description": "Demographics", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Demographics", + }, ], "aliases": [], "sdtm_domains": [], @@ -5931,7 +7798,7 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [], + "translated_texts": [], "aliases": [], "sdtm_domains": [], "items": [ @@ -6042,7 +7909,7 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [], + "translated_texts": [], "aliases": [], "sdtm_domains": [], "items": [ @@ -6114,7 +7981,7 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [], + "translated_texts": [], "aliases": [], "sdtm_domains": [], "items": [ @@ -6216,14 +8083,12 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Exposure as Collected", + "text_type": "Description", "language": "en", - "description": "Exposure as Collected", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Exposure as Collected", + }, ], "aliases": [], "sdtm_domains": [], @@ -6371,14 +8236,12 @@ "origin": "", "purpose": "Tabulation", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Vital Signs", + "text_type": "Description", "language": "en", - "description": "Vital Signs", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Vital Signs", + }, ], "aliases": [], "sdtm_domains": [], @@ -6485,14 +8348,17 @@ "sds_var_name": "DM:ETHNIC", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Subject self-reported ethnicity", + "text_type": "Description", "language": "en", - "description": "Ethnicity", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Ethnicity", + }, + { + "text_type": "Question", + "language": "en", + "text": "Subject self-reported ethnicity", + }, ], "aliases": [], "unit_definitions": [], @@ -6523,14 +8389,17 @@ "sds_var_name": "DM:RACE", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Subject self-reported race - American Indian Or Alaska Native", + "text_type": "Description", "language": "en", - "description": "Race - American Indian Or Alaska Native", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Race - American Indian Or Alaska Native", + }, + { + "text_type": "Question", + "language": "en", + "text": "Subject self-reported race - American Indian Or Alaska Native", + }, ], "aliases": [], "unit_definitions": [], @@ -6561,14 +8430,17 @@ "sds_var_name": "DM:RACE", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Subject self-reported race - Asian", + "text_type": "Description", "language": "en", - "description": "Race - Asian", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Race - Asian", + }, + { + "text_type": "Question", + "language": "en", + "text": "Subject self-reported race - Asian", + }, ], "aliases": [], "unit_definitions": [], @@ -6599,14 +8471,17 @@ "sds_var_name": "DM:RACE", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Subject self-reported race - Black or African American", + "text_type": "Description", "language": "en", - "description": "Race - Black Or African American", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Race - Black Or African American", + }, + { + "text_type": "Question", + "language": "en", + "text": "Subject self-reported race - Black or African American", + }, ], "aliases": [], "unit_definitions": [], @@ -6637,14 +8512,17 @@ "sds_var_name": "DM:RACE", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Subject self-reported race - Native Hawaiian or Other Pacific Islander", + "text_type": "Description", "language": "en", - "description": "Race - Native Hawaiian Or Other Pacific Islander", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Race - Native Hawaiian Or Other Pacific Islander", + }, + { + "text_type": "Question", + "language": "en", + "text": "Subject self-reported race - Native Hawaiian or Other Pacific Islander", + }, ], "aliases": [], "unit_definitions": [], @@ -6675,14 +8553,17 @@ "sds_var_name": "DM:RACE", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Subject self-reported race - White", + "text_type": "Description", "language": "en", - "description": "Race - White", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Race - White", + }, + { + "text_type": "Question", + "language": "en", + "text": "Subject self-reported race - White", + }, ], "aliases": [], "unit_definitions": [], @@ -6713,22 +8594,27 @@ "sds_var_name": "DM:SEX", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Sex", + "text_type": "Description", "language": "en", - "description": "Sex", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Sex", + }, + { + "text_type": "Question", + "language": "en", + "text": "Sex", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000005", "name": "DM Sex", + "version": "1.0", "submission_value": "SEX", "preferred_term": "DM Sex", + "allows_multi_choice": False, }, "terms": [ { @@ -6775,20 +8661,24 @@ "sds_var_name": "DM:AGE", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Age", + "text_type": "Description", "language": "en", - "description": "Age", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Age", + }, + { + "text_type": "Question", + "language": "en", + "text": "Age", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -6826,14 +8716,12 @@ "sds_var_name": "DM:PREVUBJ", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Previous Subject No.", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Previous Subject No.", + }, ], "aliases": [], "unit_definitions": [], @@ -6864,22 +8752,22 @@ "sds_var_name": "DS:DSCAT", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "DSCAT", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "DSCAT", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000002", "name": "DS_DSCAT", + "version": "1.0", "submission_value": "DSCAT", "preferred_term": "DS_DSCAT", + "allows_multi_choice": False, }, "terms": [ { @@ -6935,22 +8823,22 @@ "sds_var_name": "DS:DSDECOD", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "DSDECOD", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "DSDECOD", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000003", "name": "DS_DSTERM", + "version": "1.0", "submission_value": "NCOMPLT", "preferred_term": "DS_DSTERM", + "allows_multi_choice": False, }, "terms": [ { @@ -7060,14 +8948,12 @@ "sds_var_name": "DS:DSSTDTC", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date and time informed consent obtained", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Date and time informed consent obtained", + }, ], "aliases": [], "unit_definitions": [], @@ -7098,22 +8984,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000008", "name": "EC_ECTRT", + "version": "1.0", "submission_value": "Name of Treatment", "preferred_term": "EC_ECTRT", + "allows_multi_choice": False, }, "terms": [ { @@ -7169,22 +9055,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "ECCAT", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "ECCAT", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000006", "name": "EC_ECCAT", + "version": "1.0", "submission_value": "Category of Treatment", "preferred_term": "EC_ECCAT", + "allows_multi_choice": False, }, "terms": [ { @@ -7222,22 +9108,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "ECMOOD", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "ECMOOD", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000007", "name": "EC_ECMOOD", + "version": "1.0", "submission_value": "Mood", "preferred_term": "EC_ECMOOD", + "allows_multi_choice": False, }, "terms": [ { @@ -7284,22 +9170,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "ECPRESP", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "ECPRESP", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000001", "name": "No Yes Response", + "version": "1.0", "submission_value": "NY", "preferred_term": "No Yes Response", + "allows_multi_choice": False, }, "terms": [ { @@ -7337,22 +9223,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Has the investigational medicinal product been administered to the subject?", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Has the investigational medicinal product been administered to the subject?", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000001", "name": "No Yes Response", + "version": "1.0", "submission_value": "NY", "preferred_term": "No Yes Response", + "allows_multi_choice": False, }, "terms": [ { @@ -7399,14 +9285,12 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Administered by", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Administered by", + }, ], "aliases": [], "unit_definitions": [], @@ -7437,14 +9321,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Bleeding episode no.", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Bleeding episode no.", + }, ], "aliases": [], "unit_definitions": [], @@ -7475,14 +9357,12 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Checked by", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Checked by", + }, ], "aliases": [], "unit_definitions": [], @@ -7513,14 +9393,12 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Comment", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Comment", + }, ], "aliases": [], "unit_definitions": [], @@ -7551,14 +9429,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Start date and time of prescription", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Start date and time of prescription", + }, ], "aliases": [], "unit_definitions": [], @@ -7589,14 +9465,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Actual Dose", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Actual Dose", + }, ], "aliases": [], "unit_definitions": [], @@ -7627,22 +9501,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Dose form", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Dose form", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000009", "name": "EC_dose_form", + "version": "1.0", "submission_value": "FRM", "preferred_term": "EC_dose_form", + "allows_multi_choice": False, }, "terms": [ { @@ -7689,14 +9563,12 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "DUN (Dispensing Unit No.)", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "DUN (Dispensing Unit No.)", + }, ], "aliases": [], "unit_definitions": [], @@ -7727,14 +9599,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "End date and time of administration", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "End date and time of administration", + }, ], "aliases": [], "unit_definitions": [], @@ -7765,22 +9635,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Injection site", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Injection site", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000013", "name": "EC_injection_site", + "version": "1.0", "submission_value": "LOC", "preferred_term": "EC_injection_site", + "allows_multi_choice": False, }, "terms": [ { @@ -7836,22 +9706,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Laterality", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Laterality", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000012", "name": "EC_laterality", + "version": "1.0", "submission_value": "LAT", "preferred_term": "EC_laterality", + "allows_multi_choice": False, }, "terms": [ { @@ -7898,14 +9768,12 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Morphology of injection site", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Morphology of injection site", + }, ], "aliases": [], "unit_definitions": [], @@ -7936,14 +9804,12 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Number of injections", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Number of injections", + }, ], "aliases": [], "unit_definitions": [], @@ -7974,14 +9840,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Prescribed dose", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Prescribed dose", + }, ], "aliases": [], "unit_definitions": [], @@ -8012,22 +9876,22 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Requirements for dosing met?", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Requirements for dosing met?", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000001", "name": "No Yes Response", + "version": "1.0", "submission_value": "NY", "preferred_term": "No Yes Response", + "allows_multi_choice": False, }, "terms": [ { @@ -8074,22 +9938,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Route", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Route", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000011", "name": "EC_route", + "version": "1.0", "submission_value": "ROUTE", "preferred_term": "EC_route", + "allows_multi_choice": False, }, "terms": [ { @@ -8136,14 +10000,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Seq. no.", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Seq. no.", + }, ], "aliases": [], "unit_definitions": [], @@ -8174,14 +10036,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Specify reason", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Specify reason", + }, ], "aliases": [], "unit_definitions": [], @@ -8212,14 +10072,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Start date and time of administration", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Start date and time of administration", + }, ], "aliases": [], "unit_definitions": [], @@ -8250,14 +10108,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Surgery no.", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Surgery no.", + }, ], "aliases": [], "unit_definitions": [], @@ -8288,22 +10144,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Type of treatment", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Type of treatment", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000010", "name": "EC_type_of_treatment", + "version": "1.0", "submission_value": "TREATMENT TYPE", "preferred_term": "EC_type_of_treatment", + "allows_multi_choice": False, }, "terms": [ { @@ -8386,20 +10242,19 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Volume administered", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Volume administered", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -8437,14 +10292,12 @@ "sds_var_name": "", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "ICF Notes", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "ICF Notes", + }, ], "aliases": [], "unit_definitions": [], @@ -8475,14 +10328,12 @@ "sds_var_name": "DS:ICFVER", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "ICF Version", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "ICF Version", + }, ], "aliases": [], "unit_definitions": [], @@ -8513,20 +10364,19 @@ "sds_var_name": "BMI", "origin": "Protocol", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "BMI", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "BMI", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -8564,22 +10414,22 @@ "sds_var_name": "VS:VSCAT", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "VSCAT", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "VSCAT", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000004", "name": "VS_VSCAT", + "version": "1.0", "submission_value": "VSCAT", "preferred_term": "VS_VSCAT", + "allows_multi_choice": False, }, "terms": [ { @@ -8626,22 +10476,22 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "VSCAT", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "VSCAT", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000004", "name": "VS_VSCAT", + "version": "1.0", "submission_value": "VSCAT", "preferred_term": "VS_VSCAT", + "allows_multi_choice": False, }, "terms": [ { @@ -8688,14 +10538,12 @@ "sds_var_name": "VS:VSDTC", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date and time of examination", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Date and time of examination", + }, ], "aliases": [], "unit_definitions": [], @@ -8726,14 +10574,12 @@ "sds_var_name": "", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Date of examination", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Date of examination", + }, ], "aliases": [], "unit_definitions": [], @@ -8764,20 +10610,19 @@ "sds_var_name": "DIABP", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Diastolic blood pressure", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Diastolic blood pressure", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -8815,22 +10660,22 @@ "sds_var_name": "VS:FASTING", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Was the subject fasting when the body measurement was done?", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Was the subject fasting when the body measurement was done?", + }, ], "aliases": [], "unit_definitions": [], "codelist": { "uid": "CTCodelist_000001", "name": "No Yes Response", + "version": "1.0", "submission_value": "NY", "preferred_term": "No Yes Response", + "allows_multi_choice": False, }, "terms": [ { @@ -8877,20 +10722,19 @@ "sds_var_name": "HEIGHT", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Height", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Height", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -8928,14 +10772,12 @@ "sds_var_name": "PULSE", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Pulse", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Pulse", + }, ], "aliases": [], "unit_definitions": [], @@ -8966,20 +10808,19 @@ "sds_var_name": "SYSBP", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Systolic blood pressure", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Systolic blood pressure", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -9017,20 +10858,19 @@ "sds_var_name": "WEIGHT", "origin": "CRF", "comment": None, - "descriptions": [ + "translated_texts": [ { - "name": "Body weight", + "text_type": "Question", "language": "en", - "description": "Please update this description", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Body weight", + }, ], "aliases": [], "unit_definitions": [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": True, "order": 999999, "ucum": { @@ -9070,14 +10910,12 @@ "expression": "var HEIGHT_FORM_STUDY_EVENT = 'VISIT 1'; var HEIGHT_FORM_NAME = 'Body Measurements (with BMI)' var bmi = null; var fixedNum = itemJson.item.significantDigits + 1; var weight = findFirstItemValueByName(formJson, 'VS_weight_VSTESTCD-VSORRES'); // update Study Event and Form Name as needed to look up previously-collected data var heightForm = findFormData(HEIGHT_FORM_STUDY_EVENT, HEIGHT_FORM_NAME); var height = findFirstItemValueByName(heightForm[0],'VS_height_VSTESTCD-VSORRES') if (height && weight) { //BMI = ( Weight in Kilograms / ( Height in Meters x Height in Meters ) ) //var heightMtr = (height / 100); bmi = (Math.round((weight / (height * height)) * 10) / 10); } return {'value': bmi.toPrecision(fixedNum +1), 'measurementUnitName': 'kg/m2'};", } ], - "descriptions": [ + "translated_texts": [ { - "name": "Calculation of BMI based on height and weight", + "text_type": "Description", "language": "en", - "description": "Calculation of BMI based on height and weight", - "instruction": "Please update this instruction", - "sponsor_instruction": "Please update this sponsor instruction", - } + "text": "Calculation of BMI based on height and weight", + }, ], "aliases": [], "possible_actions": ["inactivate", "new_version"], diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/biomedical_concepts/test_activity_item_classes.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/biomedical_concepts/test_activity_item_classes.py index 5f45a060..53dffeec 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/biomedical_concepts/test_activity_item_classes.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/biomedical_concepts/test_activity_item_classes.py @@ -53,6 +53,7 @@ variable_class: VariableClass data_type_codelist: CTCodelist role_codelist: CTCodelist +response_codelist: CTCodelist dataset: Dataset dataset_variable: DatasetVariable data_model_catalogue_name: str @@ -79,6 +80,7 @@ def test_data(): global data_type_term global role_codelist global role_term + global response_codelist global variable_class global dataset_class global dataset @@ -105,6 +107,9 @@ def test_data(): role_term = TestUtils.create_ct_term( sponsor_preferred_name="Role", codelist_uid=role_codelist.codelist_uid ) + response_codelist = TestUtils.create_ct_codelist( + name="RESPONSE", submission_value="RESPONSE", extensible=True, approve=True + ) data_model = TestUtils.create_data_model() data_model_catalogue_name = TestUtils.create_data_model_catalogue() dataset_class = TestUtils.create_dataset_class( @@ -640,13 +645,16 @@ def test_edit_activity_item_class(api_client): assert res["nci_concept_id"] == "new nci concept id" assert res["display_name"] == "new display name for item class" assert res["order"] == 45 - assert res["activity_instance_classes"][0]["name"] == "Activity IC after edit" - assert res["activity_instance_classes"][0]["mandatory"] is False - assert ( - res["activity_instance_classes"][0]["is_adam_param_specific_enabled"] is False + ic_after_edit = next( + x + for x in res["activity_instance_classes"] + if x["uid"] == activity_instance_class_after_edit.uid ) - assert res["activity_instance_classes"][0]["is_additional_optional"] is True - assert res["activity_instance_classes"][0]["is_default_linked"] is True + assert ic_after_edit["name"] == "Activity IC after edit" + assert ic_after_edit["mandatory"] is False + assert ic_after_edit["is_adam_param_specific_enabled"] is False + assert ic_after_edit["is_additional_optional"] is True + assert ic_after_edit["is_default_linked"] is True assert res["version"] == "0.2" assert res["status"] == "Draft" assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -661,14 +669,16 @@ def test_edit_activity_item_class(api_client): assert get_res["nci_concept_id"] == "new nci concept id" assert get_res["display_name"] == "new display name for item class" assert get_res["order"] == 45 - assert get_res["activity_instance_classes"][1]["name"] == "Activity IC after edit" - assert get_res["activity_instance_classes"][1]["mandatory"] is False - assert ( - get_res["activity_instance_classes"][1]["is_adam_param_specific_enabled"] - is False - ) - assert get_res["activity_instance_classes"][1]["is_additional_optional"] is True - assert get_res["activity_instance_classes"][1]["is_default_linked"] is True + ic_after_edit = next( + x + for x in res["activity_instance_classes"] + if x["uid"] == activity_instance_class_after_edit.uid + ) + assert ic_after_edit["name"] == "Activity IC after edit" + assert ic_after_edit["mandatory"] is False + assert ic_after_edit["is_adam_param_specific_enabled"] is False + assert ic_after_edit["is_additional_optional"] is True + assert ic_after_edit["is_default_linked"] is True assert get_res["version"] == "0.2" assert get_res["status"] == "Draft" assert get_res["possible_actions"] == ["approve", "delete", "edit"] @@ -684,6 +694,21 @@ def test_edit_activity_item_class(api_client): assert_response_status_code(response, 200) assert res["variable_classes"] == [{"uid": variable_class.uid}] + # Edit Valid codelist mapping and verify + response = api_client.patch( + f"/activity-item-classes/{activity_item_class.uid}/valid-codelist-mappings", + json={"valid_codelist_uids": [response_codelist.codelist_uid]}, + ) + res = response.json() + assert_response_status_code(response, 200) + + response = api_client.get( + f"/activity-item-classes/{activity_item_class.uid}/datasets/{dataset.uid}/codelists?valid_codelists_for_item=True", + ) + res = response.json() + assert len(res["items"]) == 1 + assert res["items"][0]["codelist_uid"] == response_codelist.codelist_uid + def test_post_activity_item_class(api_client): response = api_client.post( @@ -824,7 +849,13 @@ def test_get_activity_item_class_codelists(api_client): }, ) - # So fetching terms with this ActivityItemClass and Dataset + # Also map the item class to a response codelist + api_client.patch( + f"/activity-item-classes/{activity_item_classes_all[0].uid}/valid-codelist-mappings", + json={"valid_codelist_uids": [response_codelist.codelist_uid]}, + ) + + # Fetching terms with this ActivityItemClass and Dataset # Will return those Variables # Which will in turn map a Codelist, whose Terms should be returned response = api_client.get( @@ -899,6 +930,15 @@ def test_get_activity_item_class_codelists(api_client): assert len(res["items"]) == 1 assert res["items"][0]["term_uids"] is None + # Finally, fetching using the valid codelists filter + # Will only return the specific codelists marked as valid + response = api_client.get( + f"/activity-item-classes/{activity_item_classes_all[0].uid}/datasets/{dataset.uid}/codelists?valid_codelists_for_item=True", + ) + res = response.json() + assert len(res["items"]) == 1 + assert res["items"][0]["codelist_uid"] == response_codelist.codelist_uid + def test_get_activity_item_class_overview(api_client: TestClient) -> None: """Test GET /activity-item-classes/{uid}/overview endpoint""" diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_codelist_term_listing.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_codelist_term_listing.py index 8b2383f6..409c95cd 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_codelist_term_listing.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_codelist_term_listing.py @@ -13,7 +13,6 @@ import pytest from fastapi.testclient import TestClient -from neomodel import db from clinical_mdr_api.main import app from clinical_mdr_api.models.controlled_terminologies.ct_codelist import CTCodelist diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_ct_codelist_attributes.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_ct_codelist_attributes.py new file mode 100644 index 00000000..4b4ef055 --- /dev/null +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_ct_codelist_attributes.py @@ -0,0 +1,715 @@ +""" +Tests for CT codelist attributes operations +""" + +# pylint: disable=unused-argument +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments + +# pytest fixture functions have other fixture functions as arguments, +# which pylint interprets as unused arguments + +import logging + +import pytest +from fastapi.testclient import TestClient + +from clinical_mdr_api.main import app +from clinical_mdr_api.tests.integration.utils.api import ( + inject_and_clear_db, + inject_base_data, +) +from clinical_mdr_api.tests.integration.utils.utils import TestUtils +from clinical_mdr_api.tests.utils.checks import assert_response_status_code + +log = logging.getLogger(__name__) + +# Global variables shared between fixtures and tests +BASE_CODELIST = None + +CATALOGUE_NAME = "SDTM CT" +SPONSOR_LIBRARY = "Sponsor" +URL = "/ct/codelists" + + +@pytest.fixture(scope="module") +def api_client(test_data): + """Create FastAPI test client""" + yield TestClient(app) + + +@pytest.fixture(scope="module") +def test_data(): + """Initialize test data""" + db_name = "ct-codelist-attributes.api" + inject_and_clear_db(db_name) + inject_base_data(inject_unit_subset=False) + + # Create a base codelist that can be reused across tests + global BASE_CODELIST + BASE_CODELIST = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Base Codelist", + submission_value="BASE_CL", + library_name=SPONSOR_LIBRARY, + approve=True, + extensible=True, + ) + + yield + + +def test_post_create_codelist(api_client): + """Test creating a new codelist with terms""" + # Create a term first using the base codelist + term1 = TestUtils.create_ct_term( + catalogue_name=CATALOGUE_NAME, + codelist_uid=BASE_CODELIST.codelist_uid, + sponsor_preferred_name="term1 preferred", + submission_value="TERM1_CREATE_CL", + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + data = { + "catalogue_names": [CATALOGUE_NAME], + "name": "name", + "parent_codelist_uid": None, + "submission_value": "CREATE_CL_SUBVAL", + "nci_preferred_name": "Nci preferred name", + "definition": "definition", + "extensible": True, + "is_ordinal": False, + "sponsor_preferred_name": "Sponsor preferred name", + "template_parameter": True, + "library_name": SPONSOR_LIBRARY, + "terms": [ + { + "term_uid": term1.term_uid, + "order": 999999, + "submission_value": "TERM1_CREATE_CL", + } + ], + } + response = api_client.post(URL, json=data) + + assert_response_status_code(response, 201) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] is not None + assert res["parent_codelist_uid"] is None + assert res["child_codelist_uids"] == [] + assert res["name"] == "name" + assert res["submission_value"] == "CREATE_CL_SUBVAL" + assert res["nci_preferred_name"] == "Nci preferred name" + assert res["definition"] == "definition" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["sponsor_preferred_name"] == "Sponsor preferred name" + assert res["template_parameter"] is True + assert res["library_name"] == SPONSOR_LIBRARY + assert res["possible_actions"] == ["new_version"] + + +def test_post_create_codelist_with_parent_codelist(api_client): + """Test creating a codelist with a parent codelist""" + # Create parent codelist + parent_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Parent Codelist", + submission_value="PARENT_CL", + library_name=SPONSOR_LIBRARY, + approve=False, # Keep as Draft + extensible=True, + ) + + data = { + "catalogue_names": [CATALOGUE_NAME], + "name": "name with parent", + "parent_codelist_uid": parent_codelist.codelist_uid, + "submission_value": "Submission value with parent", + "nci_preferred_name": "Nci preferred name with parent", + "definition": "definition", + "extensible": True, + "is_ordinal": False, + "sponsor_preferred_name": "Sponsor preferred name with parent", + "template_parameter": True, + "library_name": SPONSOR_LIBRARY, + "terms": [], + } + response = api_client.post(URL, json=data) + + assert_response_status_code(response, 201) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] is not None + assert res["parent_codelist_uid"] == parent_codelist.codelist_uid + assert res["child_codelist_uids"] == [] + assert res["name"] == "name with parent" + assert res["submission_value"] == "Submission value with parent" + assert res["nci_preferred_name"] == "Nci preferred name with parent" + assert res["definition"] == "definition" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["sponsor_preferred_name"] == "Sponsor preferred name with parent" + assert res["template_parameter"] is True + assert res["library_name"] == SPONSOR_LIBRARY + assert res["possible_actions"] == ["approve", "edit"] + + +def test_patch_draft_codelist(api_client): + """Test patching a draft codelist attributes""" + # Create a draft codelist + codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Original Name", + submission_value="ORIGINAL_SUB", + nci_preferred_name="Original NCI", + definition="Original definition", + extensible=False, + library_name=SPONSOR_LIBRARY, + approve=False, # Keep as Draft + ) + + data = { + "name": "codelist new name", + "submission_value": "new codelist submission value", + "nci_preferred_name": "new codelist preferred term", + "definition": "new codelist definition", + "extensible": True, + "change_description": "changing codelist name", + } + response = api_client.patch(f"{URL}/{codelist.codelist_uid}/attributes", json=data) + + assert_response_status_code(response, 200) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == codelist.codelist_uid + assert res["parent_codelist_uid"] is None + assert res["name"] == "codelist new name" + assert res["submission_value"] == "new codelist submission value" + assert res["nci_preferred_name"] == "new codelist preferred term" + assert res["definition"] == "new codelist definition" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["library_name"] == SPONSOR_LIBRARY + assert res["end_date"] is None + assert res["status"] == "Draft" + assert res["version"] == "0.2" + assert res["change_description"] == "changing codelist name" + assert res["author_username"] == "unknown-user@example.com" + assert res["possible_actions"] == ["approve", "edit"] + + +def test_patch_draft_codelist_that_is_not_tp1(api_client): + """Test patching a draft codelist that is not template parameter""" + # Create a draft codelist + codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Another Original Name", + submission_value="ANOTHER_ORIGINAL", + nci_preferred_name="Another Original NCI", + definition="Another Original definition", + extensible=False, + template_parameter=False, + library_name=SPONSOR_LIBRARY, + approve=False, # Keep as Draft + ) + + # First patch + data = { + "name": "not tp codelist new name", + "submission_value": "new not tp codelist submission value", + "nci_preferred_name": "new not tp codelist preferred term", + "definition": "new not tp codelist definition", + "extensible": True, + "change_description": "changing codelist name", + } + response = api_client.patch(f"{URL}/{codelist.codelist_uid}/attributes", json=data) + assert_response_status_code(response, 200) + + # Second patch + data = { + "name": "not tp codelist another new name", + "submission_value": "new not tp codelist submission value", + "nci_preferred_name": "new not tp codelist preferred term", + "definition": "new not tp codelist definition", + "extensible": True, + "change_description": "changing codelist name", + } + response = api_client.patch(f"{URL}/{codelist.codelist_uid}/attributes", json=data) + + assert_response_status_code(response, 200) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == codelist.codelist_uid + assert res["parent_codelist_uid"] is None + assert res["name"] == "not tp codelist another new name" + assert res["submission_value"] == "new not tp codelist submission value" + assert res["nci_preferred_name"] == "new not tp codelist preferred term" + assert res["definition"] == "new not tp codelist definition" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["library_name"] == SPONSOR_LIBRARY + assert res["end_date"] is None + assert res["status"] == "Draft" + assert res["version"] == "0.3" + assert res["change_description"] == "changing codelist name" + assert res["author_username"] == "unknown-user@example.com" + assert res["possible_actions"] == ["approve", "edit"] + + +def test_post_versions_codelist(api_client): + """Test creating a new version of an approved codelist""" + # Create and approve a codelist + codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="codelist attributes value1", + submission_value="codelist submission value1", + nci_preferred_name="codelist preferred term", + definition="codelist definition", + extensible=False, + library_name=SPONSOR_LIBRARY, + approve=True, # Approve it + ) + + response = api_client.post(f"{URL}/{codelist.codelist_uid}/attributes/versions") + + assert_response_status_code(response, 201) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == codelist.codelist_uid + assert res["parent_codelist_uid"] is None + assert res["child_codelist_uids"] == [] + assert res["name"] == "codelist attributes value1" + assert res["submission_value"] == "codelist submission value1" + assert res["nci_preferred_name"] == "codelist preferred term" + assert res["definition"] == "codelist definition" + assert res["extensible"] is False + assert res["is_ordinal"] is False + assert res["library_name"] == SPONSOR_LIBRARY + assert res["end_date"] is None + assert res["status"] == "Draft" + assert res["version"] == "1.1" + assert res["change_description"] == "New draft created" + assert res["author_username"] == "unknown-user@example.com" + assert res["possible_actions"] == ["approve", "edit"] + + +def test_post_approve_codelist(api_client): + """Test approving a draft codelist""" + # Create a draft codelist + codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Codelist to Approve", + submission_value="CL_TO_APPROVE", + nci_preferred_name="NCI to approve", + definition="definition to approve", + extensible=True, + library_name=SPONSOR_LIBRARY, + approve=False, # Keep as Draft + ) + + response = api_client.post(f"{URL}/{codelist.codelist_uid}/attributes/approvals") + + assert_response_status_code(response, 201) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == codelist.codelist_uid + assert res["parent_codelist_uid"] is None + assert res["name"] == "Codelist to Approve" + assert res["submission_value"] == "CL_TO_APPROVE" + assert res["nci_preferred_name"] == "NCI to approve" + assert res["definition"] == "definition to approve" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["library_name"] == SPONSOR_LIBRARY + assert res["end_date"] is None + assert res["status"] == "Final" + assert res["version"] == "1.0" + assert res["change_description"] == "Approved version" + assert res["author_username"] == "unknown-user@example.com" + assert res["possible_actions"] == ["new_version"] + + +def test_get_codelist_with_parent_codelist_uid(api_client): + """Test getting a codelist that has a parent""" + # Create parent codelist + parent_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Get Parent Codelist", + submission_value="GET_PARENT_CL", + library_name=SPONSOR_LIBRARY, + approve=False, + extensible=True, + ) + + # Create child codelist + child_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Get Child Codelist", + submission_value="GET_CHILD_CL", + nci_preferred_name="Child NCI", + definition="Child definition", + extensible=True, + sponsor_preferred_name="Child Sponsor Name", + template_parameter=True, + parent_codelist_uid=parent_codelist.codelist_uid, + library_name=SPONSOR_LIBRARY, + approve=False, + ) + + response = api_client.get(f"{URL}/{child_codelist.codelist_uid}/attributes") + + assert_response_status_code(response, 200) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == child_codelist.codelist_uid + assert res["parent_codelist_uid"] == parent_codelist.codelist_uid + assert res["child_codelist_uids"] == [] + assert res["name"] == "Get Child Codelist" + assert res["submission_value"] == "GET_CHILD_CL" + assert res["nci_preferred_name"] == "Child NCI" + assert res["definition"] == "Child definition" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["library_name"] == SPONSOR_LIBRARY + assert res["end_date"] is None + assert res["status"] == "Draft" + assert res["version"] == "0.1" + assert res["change_description"] == "Initial version" + assert res["author_username"] == "unknown-user@example.com" + assert res["possible_actions"] == ["approve", "edit"] + + +def test_post_add_term_to_codelist(api_client): + """Test adding a term to an approved codelist""" + # Create an approved codelist + codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Codelist for Term", + submission_value="CL_FOR_TERM", + sponsor_preferred_name="codelist_name_value", + library_name=SPONSOR_LIBRARY, + approve=True, + extensible=True, + ) + + # Create a term using the base codelist + term = TestUtils.create_ct_term( + catalogue_name=CATALOGUE_NAME, + codelist_uid=BASE_CODELIST.codelist_uid, + sponsor_preferred_name="Term to Add", + submission_value="TERM_ADD_TO_CL", + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + data = { + "term_uid": term.term_uid, + "order": 999999, + "submission_value": "TERM_ADD_TO_CL", + } + response = api_client.post(f"{URL}/{codelist.codelist_uid}/terms", json=data) + + assert_response_status_code(response, 201) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == codelist.codelist_uid + assert res["parent_codelist_uid"] is None + assert res["name"] == "Codelist for Term" + assert res["submission_value"] == "CL_FOR_TERM" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["sponsor_preferred_name"] == "codelist_name_value" + assert res["library_name"] == SPONSOR_LIBRARY + assert res["possible_actions"] == ["new_version"] + + +def test_add_term_to_unapproved_codelist(api_client): + """Test adding a term to an unapproved codelist""" + # Create an approved codelist + codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Unapproved codelist", + submission_value="UNAPPROVED_CL", + sponsor_preferred_name="unapproved_codelist_name_value", + library_name=SPONSOR_LIBRARY, + approve=False, + extensible=True, + ) + + # Create a term using the base codelist + term = TestUtils.create_ct_term( + catalogue_name=CATALOGUE_NAME, + codelist_uid=BASE_CODELIST.codelist_uid, + sponsor_preferred_name="Term to Add to Unapproved codelist", + submission_value="TERM_ADD_TO_UNAPPROVED_CL", + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + data = { + "term_uid": term.term_uid, + "order": 999999, + "submission_value": "TERM_ADD_TO_UNAPPROVED_CL", + } + response = api_client.post(f"{URL}/{codelist.codelist_uid}/terms", json=data) + assert_response_status_code(response, 400) + + response = api_client.post(f"{URL}/{codelist.codelist_uid}/attributes/approvals") + assert_response_status_code(response, 201) + + +def test_post_approve_child_codelist(api_client): + """Test approving a child codelist""" + # Create parent codelist (approved) + parent_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Approve Child Parent", + submission_value="APPROVE_CHILD_PARENT", + library_name=SPONSOR_LIBRARY, + approve=True, + extensible=True, + ) + + # Create child codelist (draft) + child_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="name with parent to approve", + submission_value="CHILD_TO_APPROVE_SUBVAL", + nci_preferred_name="Child NCI preferred name", + definition="Child definition for approval test", + extensible=True, + sponsor_preferred_name="Child sponsor preferred name", + template_parameter=True, + parent_codelist_uid=parent_codelist.codelist_uid, + library_name=SPONSOR_LIBRARY, + approve=False, + ) + + response = api_client.post( + f"{URL}/{child_codelist.codelist_uid}/attributes/approvals" + ) + + assert_response_status_code(response, 201) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == child_codelist.codelist_uid + assert res["parent_codelist_uid"] == parent_codelist.codelist_uid + assert res["child_codelist_uids"] == [] + assert res["name"] == "name with parent to approve" + assert res["submission_value"] == "CHILD_TO_APPROVE_SUBVAL" + assert res["nci_preferred_name"] == "Child NCI preferred name" + assert res["definition"] == "Child definition for approval test" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["library_name"] == SPONSOR_LIBRARY + assert res["end_date"] is None + assert res["status"] == "Final" + assert res["version"] == "1.0" + assert res["change_description"] == "Approved version" + assert res["author_username"] == "unknown-user@example.com" + assert res["possible_actions"] == ["new_version"] + + +def test_post_add_term_to_child_codelist(api_client): + """Test adding a term to a child codelist""" + # Create parent codelist (approved) + parent_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Add Term Child Parent", + submission_value="ADD_TERM_CHILD_PARENT", + library_name=SPONSOR_LIBRARY, + approve=True, + extensible=True, + ) + + # Create child codelist (approved) + child_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Child for term addition", + submission_value="CHILD_FOR_TERM_ADD_SUBVAL", + nci_preferred_name="Child for term addition NCI", + definition="Child definition for term addition", + extensible=True, + sponsor_preferred_name="Child for term sponsor name", + template_parameter=True, + parent_codelist_uid=parent_codelist.codelist_uid, + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + # Create a term using the parent codelist + term = TestUtils.create_ct_term( + catalogue_name=CATALOGUE_NAME, + codelist_uid=parent_codelist.codelist_uid, + sponsor_preferred_name="Term for child", + submission_value="TERM_FOR_CHILD_CL", + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + data = { + "term_uid": term.term_uid, + "submission_value": "TERM_FOR_CHILD_CL", + "order": 999999, + } + response = api_client.post(f"{URL}/{child_codelist.codelist_uid}/terms", json=data) + + assert_response_status_code(response, 201) + + res = response.json() + + assert res["catalogue_names"] == [CATALOGUE_NAME] + assert res["codelist_uid"] == child_codelist.codelist_uid + assert res["parent_codelist_uid"] == parent_codelist.codelist_uid + assert res["child_codelist_uids"] == [] + assert res["name"] == "Child for term addition" + assert res["submission_value"] == "CHILD_FOR_TERM_ADD_SUBVAL" + assert res["nci_preferred_name"] == "Child for term addition NCI" + assert res["definition"] == "Child definition for term addition" + assert res["extensible"] is True + assert res["is_ordinal"] is False + assert res["sponsor_preferred_name"] == "Child for term sponsor name" + assert res["template_parameter"] is True + assert res["library_name"] == SPONSOR_LIBRARY + assert res["possible_actions"] == ["new_version"] + + +def test_get_all_sub_codelists_that_have_the_provided_terms(api_client): + """Test getting sub-codelists that contain specific terms""" + # Create parent codelist (approved) + parent_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Sub Codelists Parent", + submission_value="SUB_CODELISTS_PARENT", + library_name=SPONSOR_LIBRARY, + approve=True, + extensible=True, + ) + + # Create child codelist (draft initially, then approved) + child_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + name="Sub child codelist", + submission_value="Sub child codelist submission value", + nci_preferred_name="Sub child NCI", + definition="Sub child definition", + extensible=True, + sponsor_preferred_name="Sub child sponsor preferred name", + template_parameter=True, + parent_codelist_uid=parent_codelist.codelist_uid, + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + # Create a term and add to child codelist + term = TestUtils.create_ct_term( + catalogue_name=CATALOGUE_NAME, + codelist_uid=child_codelist.codelist_uid, + sponsor_preferred_name="Shared Term", + submission_value="SHARED_TERM_SUB", + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + # Add term to parent codelist as well (this creates new version) + data = { + "term_uid": term.term_uid, + "submission_value": "SHARED_TERM_SUB", + "order": 999999, + } + api_client.post(f"{URL}/{parent_codelist.codelist_uid}/terms", json=data) + + # Now query for sub-codelists with this term + response = api_client.get( + f"{URL}/{parent_codelist.codelist_uid}/sub-codelists", + params={"term_uids": term.term_uid}, + ) + + assert_response_status_code(response, 200) + + res = response.json() + + assert len(res["items"]) >= 1 + # Find the child codelist in the results + child_result = next( + ( + item + for item in res["items"] + if item["codelist_uid"] == child_codelist.codelist_uid + ), + None, + ) + assert child_result is not None + assert child_result["catalogue_names"] == [CATALOGUE_NAME] + assert child_result["codelist_uid"] == child_codelist.codelist_uid + assert child_result["parent_codelist_uid"] == parent_codelist.codelist_uid + assert child_result["child_codelist_uids"] == [] + assert child_result["library_name"] == SPONSOR_LIBRARY + assert child_result["name"]["name"] == "Sub child sponsor preferred name" + assert child_result["name"]["template_parameter"] is True + assert child_result["attributes"]["name"] == "Sub child codelist" + assert ( + child_result["attributes"]["submission_value"] + == "Sub child codelist submission value" + ) + assert child_result["attributes"]["nci_preferred_name"] == "Sub child NCI" + assert child_result["attributes"]["definition"] == "Sub child definition" + assert child_result["attributes"]["extensible"] is True + + +def test_ordinal_codelist(api_client): + """Test ordinal codelist behaviour""" + ordinal_codelist = TestUtils.create_ct_codelist( + catalogue_name=CATALOGUE_NAME, + library_name=SPONSOR_LIBRARY, + is_ordinal=True, + approve=True, + ) + + # Create term in the base codelist + term = TestUtils.create_ct_term( + catalogue_name=CATALOGUE_NAME, + codelist_uid=BASE_CODELIST.codelist_uid, + sponsor_preferred_name="Not-ordinal term", + submission_value="NOT_ORDINAL_TERM", + library_name=SPONSOR_LIBRARY, + approve=True, + ) + + # Add term to ordinal codelist, without specifying ordinal + data = { + "term_uid": term.term_uid, + "submission_value": "NOT_ORDINAL_TERM", + "order": 999999, + } + response = api_client.post( + f"{URL}/{ordinal_codelist.codelist_uid}/terms", json=data + ) + assert_response_status_code(response, 400) + + data["ordinal"] = 1.0 + response = api_client.post( + f"{URL}/{ordinal_codelist.codelist_uid}/terms", json=data + ) + assert_response_status_code(response, 201) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_paired_codelists.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_paired_codelists.py index 4e199baf..212636aa 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_paired_codelists.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_paired_codelists.py @@ -14,7 +14,6 @@ import pytest from fastapi.testclient import TestClient -from neomodel import db from clinical_mdr_api.main import app from clinical_mdr_api.tests.integration.utils.api import ( @@ -133,7 +132,7 @@ def test_post_paired_codelist(api_client): "paired_codes_codelist_uid": codes_cl.codelist_uid, "submission_value": "DUMMYNAMES", "extensible": True, - "ordinal": False, + "is_ordinal": False, "template_parameter": False, "terms": [], }, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_sponsor_ct_packages.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_sponsor_ct_packages.py index beb1d6bf..967394a1 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_sponsor_ct_packages.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/controlled_terminologies/test_sponsor_ct_packages.py @@ -176,6 +176,106 @@ def test_post_sponsor_ct_package(api_client): ) +def test_post_sponsor_ct_package_duplicate_prevention(api_client): + """Test that creating a duplicate Sponsor CT Package is prevented and returns 409 with clear error message""" + db.cypher_query("CREATE CONSTRAINT FOR (p:CTPackage) REQUIRE (p.uid) IS NODE KEY") + + # Create first sponsor package + effective_date = (date.today() - timedelta(days=2)).strftime("%Y-%m-%d") + response = api_client.post( + f"{URL}/sponsor", + json={ + "extends_package": cdisc_package_name, + "effective_date": effective_date, + }, + ) + assert_response_status_code(response, 201) + first_package = response.json() + + # Attempt to create duplicate with same effective date + response = api_client.post( + f"{URL}/sponsor", + json={ + "extends_package": cdisc_package_name, + "effective_date": effective_date, + }, + ) + assert_response_status_code(response, 409) + error_response = response.json() + assert "type" in error_response + assert error_response["type"] == "AlreadyExistsException" + assert "message" in error_response + assert "sponsor" in error_response["message"].lower() + assert "ctpackage" in error_response["message"].lower() + assert "already exists" in error_response["message"].lower() + + # Verify only one package exists with this date + all_packages = api_client.get(f"{URL}?sponsor_only=true").json() + packages_with_date = [ + pkg for pkg in all_packages if pkg["effective_date"] == effective_date + ] + assert len(packages_with_date) == 1 + assert packages_with_date[0]["uid"] == first_package["uid"] + + +def test_post_sponsor_ct_package_error_message_clarity(api_client): + """Test that error message is clear and user-friendly when attempting to create duplicate package""" + db.cypher_query("CREATE CONSTRAINT FOR (p:CTPackage) REQUIRE (p.uid) IS NODE KEY") + + # Create first sponsor package with today's date + effective_date = date.today().strftime("%Y-%m-%d") + response = api_client.post( + f"{URL}/sponsor", + json={ + "extends_package": cdisc_package_name, + "effective_date": effective_date, + }, + ) + # This should fail because test_data fixture already creates one for today + assert_response_status_code(response, 409) + error_response = response.json() + + # Verify error message is clear + assert "message" in error_response + error_message = error_response["message"].lower() + assert "sponsor" in error_message + assert "ctpackage" in error_message or "ct package" in error_message + assert "already exists" in error_message or "exists" in error_message + assert "date" in error_message + + +def test_query_codelists_with_sponsor_package_robustness(api_client): + """Defensive test: Verify that querying codelists with a sponsor package name works correctly""" + db.cypher_query("CREATE CONSTRAINT FOR (p:CTPackage) REQUIRE (p.uid) IS NODE KEY") + + # Create a sponsor package + effective_date = (date.today() - timedelta(days=3)).strftime("%Y-%m-%d") + package = create_sponsor_package(api_client, effective_date) + package_name = package["name"] + + # Query codelists with the sponsor package - should work without errors + response = api_client.get( + f"ct/codelists?package={urllib.parse.quote_plus(package_name)}&page_size=10&is_sponsor=true" + ) + assert_response_status_code(response, 200) + codelists_response = response.json() + assert "items" in codelists_response + assert isinstance(codelists_response["items"], list) + + # Verify the query doesn't throw MultipleNodesReturned error + # (which would manifest as a 400 or 500 error) + assert response.status_code in [200, 201] + + # Verify we can query terms as well + response = api_client.get( + f"ct/terms?package={urllib.parse.quote_plus(package_name)}&page_size=10&is_sponsor=true" + ) + assert_response_status_code(response, 200) + terms_response = response.json() + assert "items" in terms_response + assert isinstance(terms_response["items"], list) + + def test_get_codelists_sponsor_ct_package(api_client): package = create_sponsor_package( api_client, (date.today() - timedelta(days=1)).strftime("%Y-%m-%d") @@ -245,7 +345,7 @@ def test_get_codelists_identical_sponsor_ct_package_nochanges(api_client): all_codelists = api_client.get("ct/codelists?page_size=0").json()["items"] # Validate that all codelists in sponsor package are contained as is in the full list - assert all([codelist in all_codelists for codelist in sponsor_package_codelists]) + assert all(codelist in all_codelists for codelist in sponsor_package_codelists) def test_get_terms_identical_sponsor_ct_package_nochanges(api_client): @@ -264,7 +364,7 @@ def test_get_terms_identical_sponsor_ct_package_nochanges(api_client): all_terms = api_client.get("ct/terms?page_size=0").json()["items"] # Validate that all terms in sponsor package are contained as is in the full list - assert all([codelist in all_terms for codelist in sponsor_package_terms]) + assert all(term in all_terms for term in sponsor_package_terms) def test_sponsor_ct_package_is_persistent_sponsor_codelists(api_client): diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_metadata.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_metadata.py index 40c181a7..167c8cdb 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_metadata.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_metadata.py @@ -3,16 +3,24 @@ # pytest fixture functions have other fixture functions as arguments, # which pylint interprets as unused arguments + import pytest from fastapi.testclient import TestClient from neomodel import db +from clinical_mdr_api.domains.enums import OdmTranslatedTextTypeEnum from clinical_mdr_api.main import app +from clinical_mdr_api.models.concepts.odms.odm_common_models import ( + OdmAliasModel, + OdmFormalExpressionModel, + OdmTranslatedTextModel, +) from clinical_mdr_api.tests.integration.utils.api import drop_db, inject_and_clear_db from clinical_mdr_api.tests.integration.utils.data_library import ( STARTUP_ODM_CONDITIONS, STARTUP_ODM_FORMS, ) +from clinical_mdr_api.tests.integration.utils.utils import TestUtils from clinical_mdr_api.tests.utils.checks import assert_response_status_code @@ -27,6 +35,41 @@ def test_data(): db.cypher_query(STARTUP_ODM_FORMS) db.cypher_query(STARTUP_ODM_CONDITIONS) + TestUtils.create_odm_form( + translated_texts=[ + OdmTranslatedTextModel( + text_type=OdmTranslatedTextTypeEnum.QUESTION, + language="da", + text="QuestionDA", + ), + OdmTranslatedTextModel( + text_type=OdmTranslatedTextTypeEnum.OSB_DESIGN_NOTES, + language="da", + text="Design NotesDA", + ), + OdmTranslatedTextModel( + text_type=OdmTranslatedTextTypeEnum.OSB_DISPLAY_TEXT, + language="da", + text="Display TextDA", + ), + ], + aliases=[ + OdmAliasModel(context="context", name="name1"), + OdmAliasModel(context="context", name="name2"), + OdmAliasModel(context="new context", name="name3"), + ], + approve=False, + ) + + TestUtils.create_odm_condition( + formal_expressions=[ + OdmFormalExpressionModel(context="context", expression="expression1"), + OdmFormalExpressionModel(context="context", expression="expression2"), + OdmFormalExpressionModel(context="new context", expression="expression3"), + ], + approve=False, + ) + yield drop_db("odm.metadata") @@ -35,8 +78,8 @@ def test_data(): @pytest.mark.parametrize( "value, expected_result_prefix, rs_length", [ - pytest.param("nAme1", {"name": "name1"}, 1), - pytest.param("mE1", {"name": "name1"}, 1), + pytest.param("nAme1", {"name": "name1"}, 2), + pytest.param("mE1", {"name": "name1"}, 2), pytest.param("cOntext1", {"context": "context1"}, 1), pytest.param("eXt1", {"context": "context1"}, 1), pytest.param("wrong", {}, 0), @@ -59,29 +102,98 @@ def test_get_aliases( @pytest.mark.parametrize( - "value, expected_result_prefix, rs_length", + "value, expected_order", [ - pytest.param("nAme1", {"name": "name1"}, 1), - pytest.param("mE1", {"name": "name1"}, 1), - pytest.param("eN", {"language": "en"}, 1), - pytest.param("N", {"language": "en"}, 1), - pytest.param("dEscription1", {"description": "description1"}, 1), - pytest.param("ptIon1", {"description": "description1"}, 1), - pytest.param("inStruction1", {"instruction": "instruction1"}, 1), - pytest.param("ctIon1", {"instruction": "instruction1"}, 1), pytest.param( - "spOnsor_instruction1", {"sponsor_instruction": "sponsor_instruction1"}, 1 + "name", + {"name": ["name1", "name1", "name2", "name3"]}, + ), + pytest.param( + "-name", + {"name": ["name3", "name2", "name1", "name1"]}, + ), + pytest.param( + "name,context", + { + "name": ["name1", "name1", "name2", "name3"], + "context": [ + "context", + "context1", + "context", + "new context", + ], + }, + ), + pytest.param( + "name,-context", + { + "name": ["name1", "name1", "name2", "name3"], + "context": [ + "context1", + "context", + "context", + "new context", + ], + }, + ), + pytest.param( + "-name,context", + { + "name": ["name3", "name2", "name1", "name1"], + "context": [ + "new context", + "context", + "context", + "context1", + ], + }, ), pytest.param( - "_instrUction1", {"sponsor_instruction": "sponsor_instruction1"}, 1 + "-name,-context", + { + "name": ["name3", "name2", "name1", "name1"], + "context": [ + "new context", + "context", + "context1", + "context", + ], + }, ), + ], +) +def test_get_aliases_in_order( + api_client, value: str, expected_order: dict[str, list[str]] +): + response = api_client.get(f"/concepts/odms/metadata/aliases?sort_by={value}") + data = response.json() + + assert_response_status_code(response, 200) + + assert len(data["items"]) == 4 + assert data["total"] == 4 + + for idx, item in enumerate(data["items"]): + for field in expected_order.keys(): + assert item[field] == expected_order[field][idx] + + +@pytest.mark.parametrize( + "value, expected_result_prefix, rs_length", + [ + pytest.param("tIOn1", {"text": "Description"}, 1), + pytest.param("eN", {"language": "en"}, 4), + pytest.param(" noTes1", {"text": "Design Notes1"}, 1), + pytest.param(" instrUctions1", {"text": "Completion Instructions1"}, 1), pytest.param("wrong", {}, 0), ], ) -def test_get_descriptions( +def test_get_translated_texts( api_client, value: str, expected_result_prefix: dict[str, str], rs_length: int ): - response = api_client.get(f"/concepts/odms/metadata/descriptions?search={value}") + response = api_client.get( + f"/concepts/odms/metadata/translated-texts?search={value}" + ) data = response.json() assert_response_status_code(response, 200) @@ -94,13 +206,104 @@ def test_get_descriptions( assert item[key].startswith(val) +@pytest.mark.parametrize( + "value, expected_order", + [ + pytest.param( + "language", + {"language": ["da", "da", "da", "en", "en", "en", "en"]}, + ), + pytest.param( + "-language", + {"language": ["en", "en", "en", "en", "da", "da", "da"]}, + ), + pytest.param( + "language,text_type", + { + "language": ["da", "da", "da", "en", "en", "en", "en"], + "text_type": [ + "osb:DesignNotes", + "osb:DisplayText", + "Question", + "Description", + "osb:CompletionInstructions", + "osb:DesignNotes", + "osb:DisplayText", + ], + }, + ), + pytest.param( + "language,-text_type", + { + "language": ["da", "da", "da", "en", "en", "en", "en"], + "text_type": [ + "Question", + "osb:DisplayText", + "osb:DesignNotes", + "osb:DisplayText", + "osb:DesignNotes", + "osb:CompletionInstructions", + "Description", + ], + }, + ), + pytest.param( + "-language,text_type", + { + "language": ["en", "en", "en", "en", "da", "da", "da"], + "text_type": [ + "Description", + "osb:CompletionInstructions", + "osb:DesignNotes", + "osb:DisplayText", + "osb:DesignNotes", + "osb:DisplayText", + "Question", + ], + }, + ), + pytest.param( + "-language,-text_type", + { + "language": ["en", "en", "en", "en", "da", "da", "da"], + "text_type": [ + "osb:DisplayText", + "osb:DesignNotes", + "osb:CompletionInstructions", + "Description", + "Question", + "osb:DisplayText", + "osb:DesignNotes", + ], + }, + ), + ], +) +def test_get_translated_texts_in_order( + api_client, value: str, expected_order: dict[str, list[str]] +): + response = api_client.get( + f"/concepts/odms/metadata/translated-texts?sort_by={value}" + ) + data = response.json() + + assert_response_status_code(response, 200) + + assert len(data["items"]) == 7 + assert data["total"] == 7 + + for idx, item in enumerate(data["items"]): + for field in expected_order.keys(): + assert item[field] == expected_order[field][idx] + + @pytest.mark.parametrize( "value, expected_result_prefix, rs_length", [ pytest.param("cOntext1", {"context": "context1"}, 1), pytest.param("eXt1", {"context": "context1"}, 1), - pytest.param("expreSsion1", {"expression": "expression1"}, 1), - pytest.param("sIon1", {"expression": "expression1"}, 1), + pytest.param("expreSsion1", {"expression": "expression1"}, 2), + pytest.param("sIon1", {"expression": "expression1"}, 2), pytest.param("wrong", {}, 0), ], ) @@ -122,6 +325,85 @@ def test_get_formal_expressions( assert item[key].startswith(val) +@pytest.mark.parametrize( + "value, expected_order", + [ + pytest.param( + "context", + {"context": ["context", "context", "context1", "new context"]}, + ), + pytest.param( + "-context", + {"context": ["new context", "context1", "context", "context"]}, + ), + pytest.param( + "context,expression", + { + "context": ["context", "context", "context1", "new context"], + "expression": [ + "expression1", + "expression2", + "expression1", + "expression3", + ], + }, + ), + pytest.param( + "context,-expression", + { + "context": ["context", "context", "context1", "new context"], + "expression": [ + "expression2", + "expression1", + "expression1", + "expression3", + ], + }, + ), + pytest.param( + "-context,expression", + { + "context": ["new context", "context1", "context", "context"], + "expression": [ + "expression3", + "expression1", + "expression1", + "expression2", + ], + }, + ), + pytest.param( + "-context,-expression", + { + "context": ["new context", "context1", "context", "context"], + "expression": [ + "expression3", + "expression1", + "expression2", + "expression1", + ], + }, + ), + ], +) +def test_get_formal_expressions_in_order( + api_client, value: str, expected_order: dict[str, list[str]] +): + response = api_client.get( + f"/concepts/odms/metadata/formal-expressions?sort_by={value}" + ) + data = response.json() + + assert_response_status_code(response, 200) + + assert len(data["items"]) == 4 + assert data["total"] == 4 + + for idx, item in enumerate(data["items"]): + for field in expected_order.keys(): + assert item[field] == expected_order[field][idx] + + def test_doesnt_return_aliases_that_are_only_connected_to_deleted_odms(api_client): data = { "library_name": "Sponsor", @@ -129,46 +411,61 @@ def test_doesnt_return_aliases_that_are_only_connected_to_deleted_odms(api_clien "oid": "oid", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [], - "aliases": [{"context": "connected to deleted", "name": "deleted"}], + "translated_texts": [], + "aliases": [{"context": "connected to be deleted", "name": "deleted"}], } response = api_client.post("concepts/odms/forms", json=data) assert_response_status_code(response, 201) rs = response.json() - response = api_client.get("/concepts/odms/metadata/aliases?page_size=1000") + response = api_client.get("/concepts/odms/metadata/aliases?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert any(item["context"] == "connected to deleted" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert any(item["context"] == "connected to be deleted" for item in data["items"]) + assert data["total"] == 5 + assert len(data["items"]) == 5 response = api_client.delete(f"concepts/odms/forms/{rs["uid"]}") assert_response_status_code(response, 204) - response = api_client.get("/concepts/odms/metadata/aliases?page_size=1000") + response = api_client.get("/concepts/odms/metadata/aliases?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert all(item["context"] != "connected to deleted" for item in data["items"]) - assert data["total"] == 1 - assert len(data["items"]) == 1 + assert all(item["context"] != "connected to be deleted" for item in data["items"]) + assert data["total"] == 4 + assert len(data["items"]) == 4 -def test_doesnt_return_descriptions_that_are_only_connected_to_deleted_odms(api_client): +def test_doesnt_return_translated_texts_that_are_only_connected_to_deleted_odms( + api_client, +): data = { "library_name": "Sponsor", "name": "to be deleted2", "oid": "oid", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "en", + "text": "Description1", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "Completion Instructions1", + }, { - "name": "connected to deleted", - "language": "eng", - "description": "description", - "instruction": "instruction", - "sponsor_instruction": "sponsor_instruction", - } + "text_type": "osb:DesignNotes", + "language": "en", + "text": "Design Notes1", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "connected to be deleted", + }, ], "aliases": [], } @@ -176,22 +473,22 @@ def test_doesnt_return_descriptions_that_are_only_connected_to_deleted_odms(api_ assert_response_status_code(response, 201) rs = response.json() - response = api_client.get("/concepts/odms/metadata/descriptions?page_size=1000") + response = api_client.get("/concepts/odms/metadata/translated-texts?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert any(item["name"] == "connected to deleted" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert any(item["text"] == "connected to be deleted" for item in data["items"]) + assert data["total"] == 8 + assert len(data["items"]) == 8 response = api_client.delete(f"concepts/odms/forms/{rs["uid"]}") assert_response_status_code(response, 204) - response = api_client.get("/concepts/odms/metadata/descriptions?page_size=1000") + response = api_client.get("/concepts/odms/metadata/translated-texts?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert all(item["name"] != "connected to deleted" for item in data["items"]) - assert data["total"] == 1 - assert len(data["items"]) == 1 + assert all(item["text"] != "connected to be deleted" for item in data["items"]) + assert data["total"] == 7 + assert len(data["items"]) == 7 def test_doesnt_return_formal_expressions_that_are_only_connected_to_deleted_odms( @@ -202,35 +499,31 @@ def test_doesnt_return_formal_expressions_that_are_only_connected_to_deleted_odm "name": "to be deleted1", "oid": "oid", "formal_expressions": [ - {"context": "connected to deleted", "expression": "expression"} + {"context": "connected to be deleted", "expression": "expression"} ], - "descriptions": [], + "translated_texts": [], "aliases": [], } response = api_client.post("concepts/odms/conditions", json=data) assert_response_status_code(response, 201) rs = response.json() - response = api_client.get( - "/concepts/odms/metadata/formal-expressions?page_size=1000" - ) + response = api_client.get("/concepts/odms/metadata/formal-expressions?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert any(item["context"] == "connected to deleted" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert any(item["context"] == "connected to be deleted" for item in data["items"]) + assert data["total"] == 5 + assert len(data["items"]) == 5 response = api_client.delete(f"concepts/odms/conditions/{rs["uid"]}") assert_response_status_code(response, 204) - response = api_client.get( - "/concepts/odms/metadata/formal-expressions?page_size=1000" - ) + response = api_client.get("/concepts/odms/metadata/formal-expressions?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert all(item["context"] != "connected to deleted" for item in data["items"]) - assert data["total"] == 1 - assert len(data["items"]) == 1 + assert all(item["context"] != "connected to be deleted" for item in data["items"]) + assert data["total"] == 4 + assert len(data["items"]) == 4 def test_doesnt_return_aliases_that_are_not_connected_to_latest_odms(api_client): @@ -242,19 +535,19 @@ def test_doesnt_return_aliases_that_are_not_connected_to_latest_odms(api_client) "oid": "oid", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [], + "translated_texts": [], "aliases": [{"context": "connected to be renamed", "name": "renaming"}], }, ) assert_response_status_code(response, 201) rs = response.json() - response = api_client.get("/concepts/odms/metadata/aliases?page_size=1000") + response = api_client.get("/concepts/odms/metadata/aliases?page_size=0") assert_response_status_code(response, 200) data = response.json() assert any(item["context"] == "connected to be renamed" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert data["total"] == 5 + assert len(data["items"]) == 5 response = api_client.patch( f"concepts/odms/forms/{rs["uid"]}", @@ -264,22 +557,27 @@ def test_doesnt_return_aliases_that_are_not_connected_to_latest_odms(api_client) "oid": "oid", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [], + "translated_texts": [], "aliases": [{"context": "done", "name": "renamed"}], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [], "change_description": "Updating alias", }, ) assert_response_status_code(response, 200) - response = api_client.get("/concepts/odms/metadata/aliases?page_size=1000") + response = api_client.get("/concepts/odms/metadata/aliases?page_size=0") assert_response_status_code(response, 200) data = response.json() assert all(item["context"] != "connected to be renamed" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert data["total"] == 5 + assert len(data["items"]) == 5 -def test_doesnt_return_descriptions_that_are_not_connected_to_latest_odms(api_client): +def test_doesnt_return_translated_texts_that_are_not_connected_to_latest_odms( + api_client, +): response = api_client.post( "concepts/odms/forms", json={ @@ -288,14 +586,27 @@ def test_doesnt_return_descriptions_that_are_not_connected_to_latest_odms(api_cl "oid": "oid", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "en", + "text": "Description1", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "Completion Instructions1", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "Design Notes1", + }, { - "name": "connected to be renamed", - "language": "eng", - "description": "description", - "instruction": "instruction", - "sponsor_instruction": "sponsor_instruction", - } + "text_type": "osb:DisplayText", + "language": "en", + "text": "connected to be renamed", + }, ], "aliases": [], }, @@ -303,12 +614,12 @@ def test_doesnt_return_descriptions_that_are_not_connected_to_latest_odms(api_cl assert_response_status_code(response, 201) rs = response.json() - response = api_client.get("/concepts/odms/metadata/descriptions?page_size=1000") + response = api_client.get("/concepts/odms/metadata/translated-texts?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert any(item["name"] == "connected to be renamed" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert any(item["text"] == "connected to be renamed" for item in data["items"]) + assert data["total"] == 8 + assert len(data["items"]) == 8 response = api_client.patch( f"concepts/odms/forms/{rs["uid"]}", @@ -318,27 +629,43 @@ def test_doesnt_return_descriptions_that_are_not_connected_to_latest_odms(api_cl "oid": "oid", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ { - "name": "renamed", - "language": "eng", - "description": "description", - "instruction": "instruction", - "sponsor_instruction": "sponsor_instruction", - } + "text_type": "Description", + "language": "en", + "text": "Description1", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "en", + "text": "Completion Instructions1", + }, + { + "text_type": "osb:DesignNotes", + "language": "en", + "text": "Design Notes1", + }, + { + "text_type": "osb:DisplayText", + "language": "en", + "text": "renamed", + }, ], "aliases": [], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [], "change_description": "Updating description", }, ) assert_response_status_code(response, 200) - response = api_client.get("/concepts/odms/metadata/descriptions?page_size=1000") + response = api_client.get("/concepts/odms/metadata/translated-texts?page_size=0") assert_response_status_code(response, 200) data = response.json() - assert all(item["name"] != "connected to be renamed" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert all(item["text"] != "connected to be renamed" for item in data["items"]) + assert data["total"] == 8 + assert len(data["items"]) == 8 def test_doesnt_return_formal_expressions_that_are_not_connected_to_latest_odms( @@ -353,21 +680,19 @@ def test_doesnt_return_formal_expressions_that_are_not_connected_to_latest_odms( "formal_expressions": [ {"context": "connected to be renamed", "expression": "renaming"} ], - "descriptions": [], + "translated_texts": [], "aliases": [], }, ) assert_response_status_code(response, 201) rs = response.json() - response = api_client.get( - "/concepts/odms/metadata/formal-expressions?page_size=1000" - ) + response = api_client.get("/concepts/odms/metadata/formal-expressions?page_size=0") assert_response_status_code(response, 200) data = response.json() assert any(item["context"] == "connected to be renamed" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert data["total"] == 5 + assert len(data["items"]) == 5 response = api_client.patch( f"concepts/odms/conditions/{rs["uid"]}", @@ -376,18 +701,16 @@ def test_doesnt_return_formal_expressions_that_are_not_connected_to_latest_odms( "name": "to be renamed1", "oid": "oid", "formal_expressions": [{"context": "renamed", "expression": "done"}], - "descriptions": [], + "translated_texts": [], "aliases": [], "change_description": "Updating formal expression", }, ) assert_response_status_code(response, 200) - response = api_client.get( - "/concepts/odms/metadata/formal-expressions?page_size=1000" - ) + response = api_client.get("/concepts/odms/metadata/formal-expressions?page_size=0") assert_response_status_code(response, 200) data = response.json() assert all(item["context"] != "connected to be renamed" for item in data["items"]) - assert data["total"] == 2 - assert len(data["items"]) == 2 + assert data["total"] == 5 + assert len(data["items"]) == 5 diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_to_activity_instance_rel.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_to_activity_instance_rel.py index 5604ff3e..2b27c017 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_to_activity_instance_rel.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_to_activity_instance_rel.py @@ -334,10 +334,10 @@ def test_add_activity_instance_relationship_to_odm_item(api_client): "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "unit_definitions": [], - "codelist_uid": None, + "codelist": None, "terms": [], "activity_instances": [ { @@ -352,6 +352,9 @@ def test_add_activity_instance_relationship_to_odm_item(api_client): "value_dependent_map": "value_dependent_map1", } ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [], "change_description": "Adding activity instance relationship", }, ) @@ -389,6 +392,14 @@ def test_get_odm_item_with_activity_instance_relationship(api_client): res["activity_instances"][0]["activity_instance_uid"] == activity_instances[0].uid ) + assert res["activity_instances"][0]["activity_instance_name"] == "name A" + assert ( + res["activity_instances"][0]["activity_instance_adam_param_code"] + == "adam_code_a" + ) + assert ( + res["activity_instances"][0]["activity_instance_topic_code"] == "topic code A" + ) assert ( res["activity_instances"][0]["activity_item_class_uid"] == activity_item_classes[0].uid @@ -411,8 +422,11 @@ def test_activity_instance_relationship_to_odm_item(api_client): "oid": "oid1", "sdtm_version": "123", "repeating": "No", - "descriptions": [], + "translated_texts": [], "aliases": [], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [], "change_description": "Adding activity instance relationship", }, ) @@ -433,12 +447,15 @@ def test_remove_activity_instance_relationship_from_odm_item(api_client): "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "unit_definitions": [], - "codelist_uid": None, + "codelist": None, "terms": [], "activity_instances": [], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [], "change_description": "Removing activity instance relationship", }, ) @@ -463,10 +480,10 @@ def test_cannot_add_more_than_two_same_activity_instance_to_odm_item(api_client) "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "unit_definitions": [], - "codelist_uid": None, + "codelist": None, "terms": [], "activity_instances": [ { @@ -492,6 +509,9 @@ def test_cannot_add_more_than_two_same_activity_instance_to_odm_item(api_client) "value_dependent_map": "value_dependent_map2", }, ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [], "change_description": "Adding more than one same activity instance", }, ) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_versioning.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_versioning.py index bbfbaae2..7c06e5d8 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_versioning.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/odms/test_odm_versioning.py @@ -19,6 +19,13 @@ from clinical_mdr_api.models.concepts.odms.odm_item import OdmItem from clinical_mdr_api.models.concepts.odms.odm_item_group import OdmItemGroup from clinical_mdr_api.models.concepts.odms.odm_study_event import OdmStudyEvent +from clinical_mdr_api.models.concepts.odms.odm_vendor_attribute import ( + OdmVendorAttribute, +) +from clinical_mdr_api.models.concepts.odms.odm_vendor_element import OdmVendorElement +from clinical_mdr_api.models.concepts.odms.odm_vendor_namespace import ( + OdmVendorNamespace, +) from clinical_mdr_api.tests.integration.utils.api import ( inject_and_clear_db, inject_base_data, @@ -33,6 +40,9 @@ forms: list[OdmForm] item_groups: list[OdmItemGroup] items: list[OdmItem] +vendor_namespace: OdmVendorNamespace +vendor_elements: list[OdmVendorElement] +vendor_attributes: list[OdmVendorAttribute] URL = "concepts/odms" @@ -54,10 +64,15 @@ def test_data(): global forms global item_groups global items + global vendor_namespace + global vendor_elements + global vendor_attributes forms = [] item_groups = [] items = [] + vendor_elements = [] + vendor_attributes = [] study_event = TestUtils.create_odm_study_event( name="StudyEvent 1", oid="SE1", approve=False @@ -76,6 +91,79 @@ def test_data(): items.append(TestUtils.create_odm_item(name="Item 1", oid="I1", approve=False)) items.append(TestUtils.create_odm_item(name="Item 2", oid="I2", approve=False)) + vendor_namespace = TestUtils.create_odm_vendor_namespace( + name="OSB", prefix="osb", url="https://osb.example.com" + ) + + vendor_elements.append( + TestUtils.create_odm_vendor_element( + name="VEF", + compatible_types=["FormDef"], + vendor_namespace_uid=vendor_namespace.uid, + approve=False, + ) + ) + vendor_elements.append( + TestUtils.create_odm_vendor_element( + name="VEIG", + compatible_types=["ItemGroupDef"], + vendor_namespace_uid=vendor_namespace.uid, + approve=False, + ) + ) + vendor_elements.append( + TestUtils.create_odm_vendor_element( + name="VEI", + compatible_types=["ItemDef"], + vendor_namespace_uid=vendor_namespace.uid, + approve=False, + ) + ) + vendor_elements.append( + TestUtils.create_odm_vendor_element( + name="VEA", + compatible_types=["FormDef", "ItemGroupDef", "ItemDef"], + vendor_namespace_uid=vendor_namespace.uid, + approve=False, + ) + ) + + vendor_attributes.append( + TestUtils.create_odm_vendor_attribute( + name="vAF", + compatible_types=["FormDef"], + data_type="string", + vendor_namespace_uid=vendor_namespace.uid, + approve=False, + ) + ) + vendor_attributes.append( + TestUtils.create_odm_vendor_attribute( + name="vAIG", + compatible_types=["ItemGroupDef"], + data_type="string", + vendor_namespace_uid=vendor_namespace.uid, + approve=False, + ) + ) + vendor_attributes.append( + TestUtils.create_odm_vendor_attribute( + name="vAI", + compatible_types=["ItemDef"], + data_type="string", + vendor_namespace_uid=vendor_namespace.uid, + approve=False, + ) + ) + vendor_attributes.append( + TestUtils.create_odm_vendor_attribute( + name="vAE", + data_type="string", + vendor_element_uid=vendor_elements[3].uid, + approve=False, + ) + ) + yield @@ -108,6 +196,7 @@ def test_add_odm_forms_to_odm_study_event(api_client): assert res["forms"] == [ { "uid": "OdmForm_000001", + "oid": "F1", "name": "Form 1", "version": "0.1", "order_number": 1, @@ -117,6 +206,7 @@ def test_add_odm_forms_to_odm_study_event(api_client): }, { "uid": "OdmForm_000002", + "oid": "F2", "name": "Form 2", "version": "0.1", "order_number": 2, @@ -331,6 +421,7 @@ def test_approve_study_event_with_cascade_effect(api_client): assert res["forms"] == [ { "uid": "OdmForm_000001", + "oid": "F1", "name": "Form 1", "version": "1.0", "order_number": 1, @@ -340,6 +431,7 @@ def test_approve_study_event_with_cascade_effect(api_client): }, { "uid": "OdmForm_000002", + "oid": "F2", "name": "Form 2", "version": "1.0", "order_number": 2, @@ -358,6 +450,7 @@ def test_approve_study_event_with_cascade_effect(api_client): assert res["forms"] == [ { "uid": "OdmForm_000001", + "oid": "F1", "name": "Form 1", "version": "1.0", "order_number": 1, @@ -367,6 +460,7 @@ def test_approve_study_event_with_cascade_effect(api_client): }, { "uid": "OdmForm_000002", + "oid": "F2", "name": "Form 2", "version": "1.0", "order_number": 2, @@ -678,6 +772,7 @@ def test_perseverance_of_final_versions_relationship_between_study_event_and_for assert res["forms"] == [ { "uid": "OdmForm_000001", + "oid": "F1", "name": "Form 1", "version": "1.0", "order_number": 1, @@ -687,6 +782,7 @@ def test_perseverance_of_final_versions_relationship_between_study_event_and_for }, { "uid": "OdmForm_000002", + "oid": "F2", "name": "Form 2", "version": "1.0", "order_number": 2, @@ -707,6 +803,7 @@ def test_latest_perseverance_of_relationship_based_on_latest_versions(api_client assert res["forms"] == [ { "uid": "OdmForm_000001", + "oid": "F1", "name": "Form 1", "version": "1.0", "order_number": 1, @@ -716,6 +813,7 @@ def test_latest_perseverance_of_relationship_based_on_latest_versions(api_client }, { "uid": "OdmForm_000002", + "oid": "F2", "name": "Form 2", "version": "1.0", "order_number": 2, @@ -804,6 +902,7 @@ def test_latest_perseverance_of_relationship_based_on_specific_versions(api_clie assert res["forms"] == [ { "uid": "OdmForm_000001", + "oid": "F1", "name": "Form 1", "version": "1.0", "order_number": 1, @@ -813,6 +912,7 @@ def test_latest_perseverance_of_relationship_based_on_specific_versions(api_clie }, { "uid": "OdmForm_000002", + "oid": "F2", "name": "Form 2", "version": "1.0", "order_number": 2, @@ -891,3 +991,416 @@ def test_latest_perseverance_of_relationship_based_on_specific_versions(api_clie "vendor": {"attributes": []}, }, ] + + +def test_add_odm_vendor_element_to_odm_form(api_client): + response = api_client.patch( + f"concepts/odms/forms/{forms[0].uid}", + json={ + "name": forms[0].name, + "oid": forms[0].oid, + "sdtm_version": forms[0].sdtm_version, + "repeating": forms[0].repeating, + "translated_texts": [], + "aliases": [], + "vendor_elements": [{"uid": vendor_elements[0].uid, "value": "value1"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + }, + ) + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000001", "name": "VEF", "value": "value1"} + ] + + response = api_client.get(f"concepts/odms/forms/{forms[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000001", "name": "VEF", "value": "value1"} + ] + + response = api_client.patch( + "concepts/odms/vendor-elements/OdmVendorElement_000001", + json={ + "name": "VEFNew", + "compatible_types": ["FormDef"], + "change_description": "name changed to VEFNew", + }, + ) + assert_response_status_code(response, 200) + + response = api_client.get(f"concepts/odms/forms/{forms[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000001", "name": "VEFNew", "value": "value1"} + ] + + +def test_add_odm_vendor_element_to_odm_item_group(api_client): + response = api_client.patch( + f"concepts/odms/item-groups/{item_groups[0].uid}", + json={ + "name": item_groups[0].name, + "oid": item_groups[0].oid, + "repeating": item_groups[0].repeating, + "is_reference_data": item_groups[0].is_reference_data, + "sas_dataset_name": item_groups[0].sas_dataset_name, + "origin": item_groups[0].origin, + "purpose": item_groups[0].purpose, + "comment": item_groups[0].comment, + "translated_texts": [], + "aliases": [], + "sdtm_domain_uids": [], + "vendor_elements": [{"uid": vendor_elements[1].uid, "value": "value1"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + }, + ) + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000002", "name": "VEIG", "value": "value1"} + ] + + response = api_client.get(f"concepts/odms/item-groups/{item_groups[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000002", "name": "VEIG", "value": "value1"} + ] + + response = api_client.patch( + "concepts/odms/vendor-elements/OdmVendorElement_000002", + json={ + "name": "VEIGNew", + "compatible_types": ["ItemDef"], + "change_description": "name changed to VEIGNew", + }, + ) + assert_response_status_code(response, 200) + + response = api_client.get(f"concepts/odms/item-groups/{item_groups[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000002", "name": "VEIGNew", "value": "value1"} + ] + + +def test_add_odm_vendor_element_to_odm_item(api_client): + response = api_client.patch( + f"concepts/odms/items/{items[0].uid}", + json={ + "name": items[0].name, + "oid": items[0].oid, + "prompt": items[0].prompt, + "datatype": items[0].datatype, + "length": items[0].length, + "significant_digits": items[0].significant_digits, + "sas_field_name": items[0].sas_field_name, + "sds_var_name": items[0].sds_var_name, + "origin": items[0].origin, + "comment": items[0].comment, + "translated_texts": [], + "aliases": [], + "unit_definitions": [], + "codelist": None, + "terms": [], + "vendor_elements": [{"uid": vendor_elements[2].uid, "value": "value1"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + }, + ) + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000003", "name": "VEI", "value": "value1"} + ] + + response = api_client.get(f"concepts/odms/items/{items[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000003", "name": "VEI", "value": "value1"} + ] + + response = api_client.patch( + "concepts/odms/vendor-elements/OdmVendorElement_000003", + json={ + "name": "VEINew", + "compatible_types": ["ItemDef"], + "change_description": "name changed to VEINew", + }, + ) + assert_response_status_code(response, 200) + + response = api_client.get(f"concepts/odms/items/{items[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_elements"] == [ + {"uid": "OdmVendorElement_000003", "name": "VEINew", "value": "value1"} + ] + + +def test_add_odm_vendor_attribute_to_odm_form(api_client): + response = api_client.patch( + f"concepts/odms/forms/{forms[0].uid}", + json={ + "name": forms[0].name, + "oid": forms[0].oid, + "sdtm_version": forms[0].sdtm_version, + "repeating": forms[0].repeating, + "translated_texts": [], + "aliases": [], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": vendor_attributes[0].uid, "value": "value1"}], + "change_description": "desc doesnt change", + }, + ) + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000001", + "name": "vAF", + "data_type": "string", + "value_regex": None, + "value": "value1", + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + response = api_client.get(f"concepts/odms/forms/{forms[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000001", + "name": "vAF", + "data_type": "string", + "value_regex": None, + "value": "value1", + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + response = api_client.patch( + "concepts/odms/vendor-attributes/OdmVendorAttribute_000001", + json={ + "name": "vAFNew", + "compatible_types": ["FormDef"], + "data_type": "string", + "value_regex": None, + "change_description": "name changed to vAFNew", + }, + ) + assert_response_status_code(response, 200) + + response = api_client.get(f"concepts/odms/forms/{forms[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000001", + "name": "vAFNew", + "data_type": "string", + "value_regex": None, + "value": "value1", + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + +def test_add_odm_vendor_attribute_to_odm_item_group(api_client): + response = api_client.patch( + f"concepts/odms/item-groups/{item_groups[0].uid}", + json={ + "name": item_groups[0].name, + "oid": item_groups[0].oid, + "repeating": item_groups[0].repeating, + "is_reference_data": item_groups[0].is_reference_data, + "sas_dataset_name": item_groups[0].sas_dataset_name, + "origin": item_groups[0].origin, + "purpose": item_groups[0].purpose, + "comment": item_groups[0].comment, + "translated_texts": [], + "aliases": [], + "sdtm_domain_uids": [], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": vendor_attributes[1].uid, "value": "value1"}], + "change_description": "desc doesnt change", + }, + ) + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000002", + "name": "vAIG", + "value": "value1", + "data_type": "string", + "value_regex": None, + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + response = api_client.get(f"concepts/odms/item-groups/{item_groups[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000002", + "name": "vAIG", + "value": "value1", + "data_type": "string", + "value_regex": None, + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + response = api_client.patch( + "concepts/odms/vendor-attributes/OdmVendorAttribute_000002", + json={ + "name": "vAIGNew", + "compatible_types": ["ItemDef"], + "data_type": "string", + "value_regex": None, + "change_description": "name changed to vAIGNew", + }, + ) + assert_response_status_code(response, 200) + + response = api_client.get(f"concepts/odms/item-groups/{item_groups[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000002", + "name": "vAIGNew", + "value": "value1", + "data_type": "string", + "value_regex": None, + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + +def test_add_odm_vendor_attribute_to_odm_item(api_client): + response = api_client.patch( + f"concepts/odms/items/{items[0].uid}", + json={ + "name": items[0].name, + "oid": items[0].oid, + "prompt": items[0].prompt, + "datatype": items[0].datatype, + "length": items[0].length, + "significant_digits": items[0].significant_digits, + "sas_field_name": items[0].sas_field_name, + "sds_var_name": items[0].sds_var_name, + "origin": items[0].origin, + "comment": items[0].comment, + "translated_texts": [], + "aliases": [], + "unit_definitions": [], + "codelist": None, + "terms": [], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": vendor_attributes[2].uid, "value": "value1"}], + "change_description": "desc doesnt change", + }, + ) + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000003", + "name": "vAI", + "value": "value1", + "data_type": "string", + "value_regex": None, + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + response = api_client.get(f"concepts/odms/items/{items[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000003", + "name": "vAI", + "data_type": "string", + "value_regex": None, + "value": "value1", + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + response = api_client.patch( + "concepts/odms/vendor-attributes/OdmVendorAttribute_000003", + json={ + "name": "vAINew", + "compatible_types": ["ItemDef"], + "data_type": "string", + "value_regex": None, + "change_description": "name changed to vAINew", + }, + ) + assert_response_status_code(response, 200) + + response = api_client.get(f"concepts/odms/items/{items[0].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "uid": "OdmVendorAttribute_000003", + "name": "vAINew", + "value": "value1", + "data_type": "string", + "value_regex": None, + "vendor_namespace_uid": "OdmVendorNamespace_000001", + } + ] + + +def test_odm_vendor_attribute_and_odm_vendor_element(api_client): + response = api_client.patch( + f"concepts/odms/vendor-attributes/{vendor_attributes[3].uid}", + json={ + "name": "vAENew", + "data_type": "string", + "value_regex": None, + "compatible_types": [], + "vendor_element_uid": vendor_elements[3].uid, + "change_description": "name changed to vAENew", + }, + ) + assert_response_status_code(response, 200) + + response = api_client.get(f"concepts/odms/vendor-elements/{vendor_elements[3].uid}") + assert_response_status_code(response, 200) + res = response.json() + assert res["vendor_attributes"] == [ + { + "compatible_types": [], + "data_type": "string", + "name": "vAENew", + "possible_actions": [ + "approve", + "delete", + "edit", + ], + "status": "Draft", + "uid": "OdmVendorAttribute_000004", + "version": "0.2", + }, + ] diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_catalogue.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_catalogue.py index fbf26b61..22f990f0 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_catalogue.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_catalogue.py @@ -49,7 +49,7 @@ def test_get_catalogue_changes_returned_valid_data(api_client): "name": "new_name", "definition": "codelist_added", "extensible": False, - "ordinal": False, + "is_ordinal": False, }, "uid": "added_codelist_uid", "change_date": "2020-06-26T00:00:00Z", @@ -61,7 +61,7 @@ def test_get_catalogue_changes_returned_valid_data(api_client): { "value_node": { "left_only": {}, - "in_common": {"extensible": False, "ordinal": False}, + "in_common": {"extensible": False, "is_ordinal": False}, "different": {"name": {"left": "old_name", "right": "new_name"}}, "right_only": {"definition": "new_definition"}, }, @@ -77,7 +77,7 @@ def test_get_catalogue_changes_returned_valid_data(api_client): "name": "new_name", "definition": "codelist_added", "extensible": False, - "ordinal": False, + "is_ordinal": False, }, }, ] diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_codelist_attributes.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_codelist_attributes.py deleted file mode 100644 index 97ce94d5..00000000 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_codelist_attributes.py +++ /dev/null @@ -1,386 +0,0 @@ -# pylint: disable=unused-argument -# pylint: disable=redefined-outer-name -# pylint: disable=too-many-arguments - -# pytest fixture functions have other fixture functions as arguments, -# which pylint interprets as unused arguments - -import pytest -from fastapi.testclient import TestClient -from neomodel import db - -from clinical_mdr_api.main import app -from clinical_mdr_api.tests.integration.utils.api import drop_db, inject_and_clear_db -from clinical_mdr_api.tests.integration.utils.data_library import ( - STARTUP_CT_CODELISTS_ATTRIBUTES_CYPHER, - STARTUP_CT_TERM_WITHOUT_CATALOGUE, -) -from clinical_mdr_api.tests.utils.checks import assert_response_status_code - - -@pytest.fixture(scope="module") -def api_client(test_data): - yield TestClient(app) - - -@pytest.fixture(scope="module") -def test_data(): - inject_and_clear_db("old.json.test.ct.codelist.attributes") - db.cypher_query(STARTUP_CT_CODELISTS_ATTRIBUTES_CYPHER) - db.cypher_query(STARTUP_CT_TERM_WITHOUT_CATALOGUE) - - yield - - drop_db("old.json.test.ct.codelist.attributes") - - -def test_post_create_codelist(api_client): - data = { - "catalogue_names": ["SDTM CT"], - "name": "name", - "parent_codelist_uid": None, - "submission_value": "Submission value", - "nci_preferred_name": "Nci preferred name", - "definition": "definition", - "extensible": True, - "ordinal": False, - "sponsor_preferred_name": "Sponsor preferred name", - "template_parameter": True, - "library_name": "Sponsor", - "terms": [ - { - "term_uid": "term1", - "order": 999999, - "submission_value": "term1 submission value", - } - ], - } - response = api_client.post("/ct/codelists", json=data) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "CTCodelist_000001" - assert res["parent_codelist_uid"] is None - assert res["child_codelist_uids"] == [] - assert res["name"] == "name" - assert res["submission_value"] == "Submission value" - assert res["nci_preferred_name"] == "Nci preferred name" - assert res["definition"] == "definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["sponsor_preferred_name"] == "Sponsor preferred name" - assert res["template_parameter"] is True - assert res["library_name"] == "Sponsor" - assert res["possible_actions"] == ["new_version"] - - -def test_post_create_codelist_with_parent_codelist(api_client): - data = { - "catalogue_names": ["SDTM CT"], - "name": "name with parent", - "parent_codelist_uid": "ct_codelist_root3", - "submission_value": "Submission value with parent", - "nci_preferred_name": "Nci preferred name with parent", - "definition": "definition", - "extensible": True, - "ordinal": False, - "sponsor_preferred_name": "Sponsor preferred name with parent", - "template_parameter": True, - "library_name": "Sponsor", - "terms": [], - } - response = api_client.post("/ct/codelists", json=data) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "CTCodelist_000002" - assert res["parent_codelist_uid"] == "ct_codelist_root3" - assert res["child_codelist_uids"] == [] - assert res["name"] == "name with parent" - assert res["submission_value"] == "Submission value with parent" - assert res["nci_preferred_name"] == "Nci preferred name with parent" - assert res["definition"] == "definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["sponsor_preferred_name"] == "Sponsor preferred name with parent" - assert res["template_parameter"] is True - assert res["library_name"] == "Sponsor" - assert res["possible_actions"] == ["approve", "edit"] - - -def test_patch_draft_codelist(api_client): - data = { - "name": "codelist new name", - "submission_value": "new codelist submission value", - "nci_preferred_name": "new codelist preferred term", - "definition": "new codelist definition", - "extensible": True, - "change_description": "changing codelist name", - } - response = api_client.patch("/ct/codelists/ct_codelist_root3/attributes", json=data) - - assert_response_status_code(response, 200) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "ct_codelist_root3" - assert res["parent_codelist_uid"] is None - assert res["child_codelist_uids"] == ["CTCodelist_000002"] - assert res["name"] == "codelist new name" - assert res["submission_value"] == "new codelist submission value" - assert res["nci_preferred_name"] == "new codelist preferred term" - assert res["definition"] == "new codelist definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["library_name"] == "Sponsor" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "changing codelist name" - assert res["author_username"] == "unknown-user@example.com" - assert res["possible_actions"] == ["approve", "edit"] - - -def test_patch_draft_codelist_that_is_not_tp1(api_client): - data = { - "name": "codelist another new name", - "submission_value": "new codelist submission value", - "nci_preferred_name": "new codelist preferred term", - "definition": "new codelist definition", - "extensible": True, - "change_description": "changing codelist name", - } - response = api_client.patch("/ct/codelists/ct_codelist_root3/attributes", json=data) - - assert_response_status_code(response, 200) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "ct_codelist_root3" - assert res["parent_codelist_uid"] is None - assert res["child_codelist_uids"] == ["CTCodelist_000002"] - assert res["name"] == "codelist another new name" - assert res["submission_value"] == "new codelist submission value" - assert res["nci_preferred_name"] == "new codelist preferred term" - assert res["definition"] == "new codelist definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["library_name"] == "Sponsor" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.3" - assert res["change_description"] == "changing codelist name" - assert res["author_username"] == "unknown-user@example.com" - assert res["possible_actions"] == ["approve", "edit"] - - -def test_post_versions_codelist2(api_client): - response = api_client.post("/ct/codelists/ct_codelist_root1/attributes/versions") - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "ct_codelist_root1" - assert res["parent_codelist_uid"] is None - assert res["child_codelist_uids"] == [] - assert res["name"] == "codelist attributes value1" - assert res["submission_value"] == "codelist submission value1" - assert res["nci_preferred_name"] == "codelist preferred term" - assert res["definition"] == "codelist definition" - assert res["extensible"] is False - assert res["ordinal"] is False - assert res["library_name"] == "Sponsor" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "1.1" - assert res["change_description"] == "New draft created" - assert res["author_username"] == "unknown-user@example.com" - assert res["possible_actions"] == ["approve", "edit"] - - -def test_post_approve_codelist(api_client): - response = api_client.post("/ct/codelists/ct_codelist_root3/attributes/approvals") - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "ct_codelist_root3" - assert res["parent_codelist_uid"] is None - assert res["child_codelist_uids"] == ["CTCodelist_000002"] - assert res["name"] == "codelist another new name" - assert res["submission_value"] == "new codelist submission value" - assert res["nci_preferred_name"] == "new codelist preferred term" - assert res["definition"] == "new codelist definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["library_name"] == "Sponsor" - assert res["end_date"] is None - assert res["status"] == "Final" - assert res["version"] == "1.0" - assert res["change_description"] == "Approved version" - assert res["author_username"] == "unknown-user@example.com" - assert res["possible_actions"] == ["new_version"] - - -def test_get_codelist_with_parent_codelist_uid(api_client): - response = api_client.get("/ct/codelists/CTCodelist_000002/attributes") - - assert_response_status_code(response, 200) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "CTCodelist_000002" - assert res["parent_codelist_uid"] == "ct_codelist_root3" - assert res["child_codelist_uids"] == [] - assert res["name"] == "name with parent" - assert res["submission_value"] == "Submission value with parent" - assert res["nci_preferred_name"] == "Nci preferred name with parent" - assert res["definition"] == "definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["library_name"] == "Sponsor" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.1" - assert res["change_description"] == "Initial version" - assert res["author_username"] == "unknown-user@example.com" - assert res["possible_actions"] == ["approve", "edit"] - - -def test_post_add_term_to_codelist(api_client): - data = { - "term_uid": "term1", - "order": 999999, - "submission_value": "term1 submission value", - } - response = api_client.post("/ct/codelists/ct_codelist_root3/terms", json=data) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "ct_codelist_root3" - assert res["parent_codelist_uid"] is None - assert res["child_codelist_uids"] == ["CTCodelist_000002"] - assert res["name"] == "codelist another new name" - assert res["submission_value"] == "new codelist submission value" - assert res["nci_preferred_name"] == "new codelist preferred term" - assert res["definition"] == "new codelist definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["sponsor_preferred_name"] == "codelist_name_value" - assert res["template_parameter"] is False - assert res["library_name"] == "Sponsor" - assert res["possible_actions"] == ["new_version"] - - -def test_post_approve_child_codelist(api_client): - response = api_client.post("/ct/codelists/CTCodelist_000002/attributes/approvals") - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "CTCodelist_000002" - assert res["parent_codelist_uid"] == "ct_codelist_root3" - assert res["child_codelist_uids"] == [] - assert res["name"] == "name with parent" - assert res["submission_value"] == "Submission value with parent" - assert res["nci_preferred_name"] == "Nci preferred name with parent" - assert res["definition"] == "definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["library_name"] == "Sponsor" - assert res["end_date"] is None - assert res["status"] == "Final" - assert res["version"] == "1.0" - assert res["change_description"] == "Approved version" - assert res["author_username"] == "unknown-user@example.com" - assert res["possible_actions"] == ["new_version"] - - -def test_post_add_term_to_child_codelist(api_client): - data = { - "term_uid": "term1", - "submission_value": "term1 submission value", - "order": 999999, - } - response = api_client.post("/ct/codelists/CTCodelist_000002/terms", json=data) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["catalogue_names"] == ["SDTM CT"] - assert res["codelist_uid"] == "CTCodelist_000002" - assert res["parent_codelist_uid"] == "ct_codelist_root3" - assert res["child_codelist_uids"] == [] - assert res["name"] == "name with parent" - assert res["submission_value"] == "Submission value with parent" - assert res["nci_preferred_name"] == "Nci preferred name with parent" - assert res["definition"] == "definition" - assert res["extensible"] is True - assert res["ordinal"] is False - assert res["sponsor_preferred_name"] == "Sponsor preferred name with parent" - assert res["template_parameter"] is True - assert res["library_name"] == "Sponsor" - assert res["possible_actions"] == ["new_version"] - - -def test_get_all_sub_codelists_that_have_the_provided_terms(api_client): - data = {"term_uid": "term1"} - response = api_client.get( - "/ct/codelists/ct_codelist_root3/sub-codelists?term_uids=term1", params=data - ) - - assert_response_status_code(response, 200) - - res = response.json() - - assert res["items"][0]["catalogue_names"] == ["SDTM CT"] - assert res["items"][0]["codelist_uid"] == "CTCodelist_000002" - assert res["items"][0]["parent_codelist_uid"] == "ct_codelist_root3" - assert res["items"][0]["child_codelist_uids"] == [] - assert res["items"][0]["library_name"] == "Sponsor" - assert res["items"][0]["name"]["name"] == "Sponsor preferred name with parent" - assert res["items"][0]["name"]["template_parameter"] is True - assert res["items"][0]["name"]["end_date"] is None - assert res["items"][0]["name"]["status"] == "Draft" - assert res["items"][0]["name"]["version"] == "0.1" - assert res["items"][0]["name"]["change_description"] == "Initial version" - assert res["items"][0]["name"]["author_username"] == "unknown-user@example.com" - assert res["items"][0]["name"]["possible_actions"] == ["approve", "edit"] - assert res["items"][0]["attributes"]["name"] == "name with parent" - assert ( - res["items"][0]["attributes"]["submission_value"] - == "Submission value with parent" - ) - assert ( - res["items"][0]["attributes"]["nci_preferred_name"] - == "Nci preferred name with parent" - ) - assert res["items"][0]["attributes"]["definition"] == "definition" - assert res["items"][0]["attributes"]["extensible"] is True - assert res["items"][0]["attributes"]["end_date"] is None - assert res["items"][0]["attributes"]["status"] == "Final" - assert res["items"][0]["attributes"]["version"] == "1.0" - assert res["items"][0]["attributes"]["change_description"] == "Approved version" - assert ( - res["items"][0]["attributes"]["author_username"] == "unknown-user@example.com" - ) - assert res["items"][0]["attributes"]["possible_actions"] == ["new_version"] diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_codelist_attributes_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_codelist_attributes_negative.py index 50852d9c..2e2828da 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_codelist_attributes_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_codelist_attributes_negative.py @@ -40,7 +40,7 @@ def test_post_create_codelist_non_enditable_library(api_client): "nci_preferred_name": "Nci preferred name", "definition": "definition", "extensible": True, - "ordinal": False, + "is_ordinal": False, "sponsor_preferred_name": "Sponsor preferred name", "template_parameter": True, "library_name": "CDISC", @@ -193,7 +193,7 @@ def test_post_create_codelist_with_parent_codelist1(api_client): "nci_preferred_name": "Nci preferred name with parent", "definition": "definition", "extensible": True, - "ordinal": False, + "is_ordinal": False, "sponsor_preferred_name": "Sponsor preferred name with parent", "template_parameter": True, "library_name": "Sponsor", @@ -214,7 +214,7 @@ def test_post_create_codelist_with_parent_codelist1(api_client): assert res["nci_preferred_name"] == "Nci preferred name with parent" assert res["definition"] == "definition" assert res["extensible"] is True - assert res["ordinal"] is False + assert res["is_ordinal"] is False assert res["sponsor_preferred_name"] == "Sponsor preferred name with parent" assert res["template_parameter"] is True assert res["library_name"] == "Sponsor" diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_package.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_package.py index 8b3b77c5..c5709002 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_package.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_package.py @@ -47,7 +47,7 @@ def test_get_packages_changes_returned_valid_data(api_client): "name": "new_name", "definition": "codelist_added", "extensible": False, - "ordinal": False, + "is_ordinal": False, }, "uid": "added_codelist_uid", "change_date": "2020-06-26T00:00:00Z", @@ -56,7 +56,11 @@ def test_get_packages_changes_returned_valid_data(api_client): ] assert res["deleted_codelists"] == [ { - "value_node": {"name": "old_name", "extensible": False, "ordinal": False}, + "value_node": { + "name": "old_name", + "extensible": False, + "is_ordinal": False, + }, "uid": "deleted_codelist_uid", "change_date": "2020-03-27T00:00:00Z", "is_change_of_codelist": True, @@ -66,7 +70,7 @@ def test_get_packages_changes_returned_valid_data(api_client): { "value_node": { "left_only": {}, - "in_common": {"extensible": False, "ordinal": False}, + "in_common": {"extensible": False, "is_ordinal": False}, "different": {"name": {"left": "old_name", "right": "new_name"}}, "right_only": {"definition": "new_definition"}, }, @@ -81,7 +85,7 @@ def test_get_packages_changes_returned_valid_data(api_client): "name": "new_name", "definition": "codelist_added", "extensible": False, - "ordinal": False, + "is_ordinal": False, }, "change_date": "2020-06-26T00:00:00Z", }, @@ -145,7 +149,7 @@ def test_get_packages_changes_for_a_specific_codelist_returned_valid_data(api_cl { "value_node": { "left_only": {}, - "in_common": {"extensible": False, "ordinal": False}, + "in_common": {"extensible": False, "is_ordinal": False}, "different": {"name": {"left": "old_name", "right": "new_name"}}, "right_only": {"definition": "new_definition"}, }, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_stats.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_stats.py index 0607fb71..abd4d1bd 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_stats.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_ct_stats.py @@ -92,7 +92,7 @@ def test_ct_stats(api_client): "nci_preferred_name": "codelist preferred term", "definition": "codelist definition", "extensible": False, - "ordinal": False, + "is_ordinal": False, "library_name": None, "start_date": "2020-06-26T00:00:00Z", "end_date": None, @@ -135,7 +135,7 @@ def test_ct_stats(api_client): "nci_preferred_name": "codelist preferred term", "definition": "codelist definition", "extensible": False, - "ordinal": False, + "is_ordinal": False, "library_name": None, "start_date": "2020-06-26T00:00:00Z", "end_date": None, @@ -178,7 +178,7 @@ def test_ct_stats(api_client): "nci_preferred_name": "codelist preferred term", "definition": "codelist definition", "extensible": False, - "ordinal": False, + "is_ordinal": False, "library_name": None, "start_date": "2020-06-26T00:00:00Z", "end_date": None, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions.py index ac6553dc..0e6b8b8d 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions.py @@ -44,20 +44,46 @@ def test_creating_a_new_odm_condition(api_client): "name": "name1", "oid": "oid1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -80,20 +106,46 @@ def test_creating_a_new_odm_condition(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -119,20 +171,46 @@ def test_getting_non_empty_list_of_odm_conditions(api_client): assert res["items"][0]["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["items"][0]["descriptions"] == [ + assert res["items"][0]["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["items"][0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -168,20 +246,46 @@ def test_getting_a_specific_odm_condition(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -207,20 +311,46 @@ def test_getting_versions_of_a_specific_odm_condition(api_client): assert res[0]["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res[0]["descriptions"] == [ + assert res[0]["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res[0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -234,20 +364,46 @@ def test_updating_an_existing_odm_condition(api_client): "oid": "oid1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], "change_description": "name changed", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -272,20 +428,46 @@ def test_updating_an_existing_odm_condition(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -313,20 +495,46 @@ def test_getting_a_specific_odm_condition_in_specific_version(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -352,20 +560,46 @@ def test_approving_an_odm_condition(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -393,20 +627,46 @@ def test_inactivating_a_specific_odm_condition(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -434,20 +694,46 @@ def test_reactivating_a_specific_odm_condition(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -473,20 +759,46 @@ def test_creating_a_new_odm_condition_version(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -499,13 +811,11 @@ def test_create_a_new_odm_condition_for_deleting_it(api_client): "name": "name - delete", "oid": "oid2", "formal_expressions": [], - "descriptions": [ + "translated_texts": [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ], "aliases": [], @@ -526,13 +836,11 @@ def test_create_a_new_odm_condition_for_deleting_it(api_client): assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" assert res["formal_expressions"] == [] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ] assert res["aliases"] == [] @@ -551,14 +859,15 @@ def test_creating_a_new_odm_condition_with_relations(api_client): "name": "string", "oid": "string", "formal_expressions": [{"context": "string", "expression": "string"}], - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string2"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, ], "aliases": [], } @@ -578,14 +887,15 @@ def test_creating_a_new_odm_condition_with_relations(api_client): assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" assert res["formal_expressions"] == [{"context": "string", "expression": "string"}] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string2"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, ] assert res["aliases"] == [] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -601,14 +911,15 @@ def test_updating_an_existing_odm_condition_with_relations(api_client): {"context": "context1", "expression": "expression1"}, {"context": "context4", "expression": "expression4"}, ], - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string3"}, { - "name": "string3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string3"}, ], "aliases": [{"context": "context1", "name": "name1"}], } @@ -633,14 +944,15 @@ def test_updating_an_existing_odm_condition_with_relations(api_client): {"context": "context1", "expression": "expression1"}, {"context": "context4", "expression": "expression4"}, ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string3"}, { - "name": "string3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string3"}, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["possible_actions"] == ["approve", "edit"] diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions_negative.py index 078812e9..4404f60d 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_conditions_negative.py @@ -34,20 +34,46 @@ def test_create_a_new_odm_condition(api_client): "name": "name1", "oid": "oid1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -70,20 +96,46 @@ def test_create_a_new_odm_condition(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -96,20 +148,46 @@ def test_cannot_create_a_new_odm_condition_with_same_properties(api_client): "name": "name1", "oid": "oid1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -133,13 +211,11 @@ def test_cannot_create_a_new_odm_condition_without_an_english_description(api_cl "name": "name2", "oid": "oid2", "formal_expressions": [], - "descriptions": [ + "translated_texts": [ { - "name": "name - non-eng", + "text_type": "Description", "language": "DAN", - "description": "description - non-eng", - "instruction": "instruction - non-eng", - "sponsor_instruction": "sponsor_instruction - non-eng", + "text": "text - non-eng", } ], "aliases": [], @@ -152,7 +228,8 @@ def test_cannot_create_a_new_odm_condition_without_an_english_description(api_cl assert res["type"] == "ValidationException" assert ( - res["message"] == "At least one description must be in English ('eng' or 'en')." + res["message"] + == "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." ) @@ -194,3 +271,42 @@ def test_cannot_reactivate_an_odm_condition_that_is_not_retired(api_client): assert res["type"] == "BusinessLogicException" assert res["message"] == "Only RETIRED version can be reactivated." + + +@pytest.mark.parametrize( + "text_type", + [ + pytest.param("Description"), + pytest.param("Question"), + pytest.param("osb:DesignNotes"), + pytest.param("osb:CompletionInstructions"), + pytest.param("osb:DisplayText"), + ], +) +def test_cannot_add_duplicate_translated_texts(api_client, text_type: str): + data = { + "library_name": "Sponsor", + "name": "testing", + "oid": "testing", + "formal_expressions": [{"context": "context1", "expression": "expression1"}], + "translated_texts": [ + { + "text_type": text_type, + "language": "eng", + "text": str(r), + } + for r in range(2) + ], + "aliases": [], + } + response = api_client.post("concepts/odms/conditions", json=data) + + assert_response_status_code(response, 400) + + res = response.json() + + assert res["type"] == "ValidationException" + assert ( + res["message"] + == f"Duplicate Translated Text found for text_type '{text_type}' and language 'eng'." + ) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms.py index e7a6029c..29db09bd 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms.py @@ -55,23 +55,54 @@ def test_creating_a_new_odm_form(api_client): "oid": "oid1", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute2", "value": "value"} + ], + "vendor_attributes": [{"uid": "odm_vendor_attribute4", "value": "value"}], } response = api_client.post("concepts/odms/forms", json=data) @@ -90,27 +121,73 @@ def test_creating_a_new_odm_form(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["item_groups"] == [] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -132,27 +209,73 @@ def test_getting_non_empty_list_of_odm_forms(api_client): assert res["items"][0]["version"] == "0.1" assert res["items"][0]["change_description"] == "Initial version" assert res["items"][0]["author_username"] == "unknown-user@example.com" - assert res["items"][0]["descriptions"] == [ + assert res["items"][0]["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["items"][0]["aliases"] == [{"context": "context1", "name": "name1"}] assert res["items"][0]["item_groups"] == [] - assert res["items"][0]["vendor_elements"] == [] - assert res["items"][0]["vendor_attributes"] == [] - assert res["items"][0]["vendor_element_attributes"] == [] + assert res["items"][0]["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["items"][0]["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["items"][0]["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["items"][0]["possible_actions"] == ["approve", "delete", "edit"] @@ -184,27 +307,72 @@ def test_getting_a_specific_odm_form(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -226,27 +394,73 @@ def test_getting_versions_of_a_specific_odm_form(api_client): assert res[0]["version"] == "0.1" assert res[0]["change_description"] == "Initial version" assert res[0]["author_username"] == "unknown-user@example.com" - assert res[0]["descriptions"] == [ + assert res[0]["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res[0]["aliases"] == [{"context": "context1", "name": "name1"}] assert res[0]["item_groups"] == [] - assert res[0]["vendor_elements"] == [] - assert res[0]["vendor_attributes"] == [] - assert res[0]["vendor_element_attributes"] == [] + assert res[0]["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res[0]["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res[0]["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res[0]["possible_actions"] == ["approve", "delete", "edit"] @@ -258,23 +472,58 @@ def test_updating_an_existing_odm_form(api_client): "sdtm_version": "0.1", "repeating": "Yes", "change_description": "repeating changed to Yes", - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [ + {"uid": "odm_vendor_element3", "value": "value"}, + ], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute7", "value": "value"}, + ], + "vendor_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"}, + ], } response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) @@ -293,27 +542,73 @@ def test_updating_an_existing_odm_form(api_client): assert res["version"] == "0.2" assert res["change_description"] == "repeating changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["item_groups"] == [] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "data_type": "string", + "name": "nameThree", + "uid": "odm_vendor_attribute3", + "value": "value", + "value_regex": "^[a-zA-Z]+$", + "vendor_namespace_uid": "odm_vendor_namespace1", + }, + ] + assert res["vendor_element_attributes"] == [ + { + "data_type": "string", + "name": "nameSeven", + "uid": "odm_vendor_attribute7", + "value": "value", + "value_regex": None, + "vendor_element_uid": "odm_vendor_element3", + }, + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -335,480 +630,69 @@ def test_getting_a_specific_odm_form_in_specific_version(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_item_groups_to_a_specific_odm_form(api_client): - data = [ - { - "uid": "odm_item_group1", - "order_number": 1, - "mandatory": "Yes", - "locked": "No", - "collection_exception_condition_oid": "collection_exception_condition_oid1", - "vendor": {"attributes": [{"uid": "odm_vendor_attribute3", "value": "No"}]}, - } - ] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/item-groups", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmForm_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["sdtm_version"] == "0.1" - assert res["repeating"] == "Yes" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [ - { - "uid": "odm_item_group1", - "oid": "oid1", - "name": "name1", - "version": "1.0", - "order_number": 1, - "mandatory": "Yes", - "collection_exception_condition_oid": "collection_exception_condition_oid1", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "No", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_item_groups_from_a_specific_odm_form(api_client): - data = [ - { - "uid": "odm_item_group2", - "order_number": 2, - "mandatory": "Yes", - "locked": "No", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": {"attributes": [{"uid": "odm_vendor_attribute3", "value": "No"}]}, - } - ] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/item-groups?override=true", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmForm_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["sdtm_version"] == "0.1" - assert res["repeating"] == "Yes" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [ - { - "uid": "odm_item_group2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "No", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_element_to_a_specific_odm_form(api_client): - data = [{"uid": "odm_vendor_element2", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-elements", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmForm_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["sdtm_version"] == "0.1" - assert res["repeating"] == "Yes" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", }, { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [ - { - "uid": "odm_item_group2", - "oid": "oid2", - "version": "1.0", - "name": "name2", - "order_number": 2, - "mandatory": "Yes", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "No", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element2", "name": "nameTwo", "value": "value"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_element_from_a_specific_odm_form(api_client): - data = [{"uid": "odm_vendor_element1", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-elements?override=true", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmForm_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["sdtm_version"] == "0.1" - assert res["repeating"] == "Yes" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [ - { - "uid": "odm_item_group2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "No", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_attribute_to_a_specific_odm_form(api_client): - data = [{"uid": "odm_vendor_attribute3", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-attributes", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmForm_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["sdtm_version"] == "0.1" - assert res["repeating"] == "Yes" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", }, { - "name": "name3", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "sponsor_instruction2", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [ { - "uid": "odm_item_group2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "No", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_attribute_from_a_specific_odm_form(api_client): - data = [{"uid": "odm_vendor_attribute4", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-attributes?override=true", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmForm_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["sdtm_version"] == "0.1" - assert res["repeating"] == "Yes" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [ - { - "uid": "odm_item_group2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "No", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", - "data_type": "string", - "value_regex": None, - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, ] + assert res["aliases"] == [{"context": "context1", "name": "name1"}] + assert res["item_groups"] == [] + assert res["vendor_elements"] == [] + assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [] assert res["possible_actions"] == ["approve", "delete", "edit"] -def test_adding_odm_vendor_element_attribute_to_a_specific_odm_form(api_client): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] +def test_adding_odm_item_groups_to_a_specific_odm_form(api_client): + data = [ + { + "uid": "odm_item_group1", + "order_number": 1, + "mandatory": "Yes", + "locked": "No", + "collection_exception_condition_oid": "collection_exception_condition_oid1", + "vendor": {"attributes": [{"uid": "odm_vendor_attribute3", "value": "No"}]}, + } + ] response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-element-attributes", json=data + "concepts/odms/forms/OdmForm_000001/item-groups", json=data ) assert_response_status_code(response, 201) @@ -826,32 +710,58 @@ def test_adding_odm_vendor_element_attribute_to_a_specific_odm_form(api_client): assert res["version"] == "0.2" assert res["change_description"] == "repeating changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["item_groups"] == [ { - "uid": "odm_item_group2", - "oid": "oid2", - "name": "name2", + "uid": "odm_item_group1", + "oid": "oid1", + "name": "name1", "version": "1.0", - "order_number": 2, + "order_number": 1, "mandatory": "Yes", - "collection_exception_condition_oid": "collection_exception_condition_oid2", + "collection_exception_condition_oid": "collection_exception_condition_oid1", "vendor": { "attributes": [ { @@ -867,36 +777,44 @@ def test_adding_odm_vendor_element_attribute_to_a_specific_odm_form(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute1", - "name": "nameOne", "data_type": "string", - "value_regex": "^[a-zA-Z]+$", + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", - "vendor_element_uid": "odm_vendor_element1", - } + "value_regex": None, + "vendor_element_uid": "odm_vendor_element3", + }, ] assert res["possible_actions"] == ["approve", "delete", "edit"] -def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_form(api_client): - data = [{"uid": "odm_vendor_attribute2", "value": "value"}] +def test_overriding_odm_item_groups_from_a_specific_odm_form(api_client): + data = [ + { + "uid": "odm_item_group2", + "order_number": 2, + "mandatory": "Yes", + "locked": "No", + "collection_exception_condition_oid": "collection_exception_condition_oid2", + "vendor": {"attributes": [{"uid": "odm_vendor_attribute3", "value": "No"}]}, + } + ] response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-element-attributes?override=true", - json=data, + "concepts/odms/forms/OdmForm_000001/item-groups?override=true", json=data ) assert_response_status_code(response, 201) @@ -914,20 +832,46 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_form(api_cl assert res["version"] == "0.2" assert res["change_description"] == "repeating changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -955,40 +899,91 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_form(api_cl } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute2", - "name": "nameTwo", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", - "vendor_element_uid": "odm_vendor_element1", - } + "value_regex": None, + "vendor_element_uid": "odm_vendor_element3", + }, ] assert res["possible_actions"] == ["approve", "delete", "edit"] def test_managing_odm_vendors_of_a_specific_odm_form(api_client): data = { - "elements": [{"uid": "odm_vendor_element3", "value": "value"}], - "element_attributes": [{"uid": "odm_vendor_attribute7", "value": "value"}], - "attributes": [{"uid": "odm_vendor_attribute4", "value": "value"}], + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "Yes", + "change_description": "repeating changed to Yes", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [{"uid": "odm_vendor_element3", "value": "value"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute7", "value": "value"} + ], + "vendor_attributes": [{"uid": "odm_vendor_attribute4", "value": "value"}], } - response = api_client.post("concepts/odms/forms/OdmForm_000001/vendors", json=data) + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) - assert_response_status_code(response, 201) + assert_response_status_code(response, 200) res = response.json() @@ -1003,20 +998,46 @@ def test_managing_odm_vendors_of_a_specific_odm_form(api_client): assert res["version"] == "0.2" assert res["change_description"] == "repeating changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1044,7 +1065,7 @@ def test_managing_odm_vendors_of_a_specific_odm_form(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { @@ -1087,20 +1108,46 @@ def test_approving_an_odm_form(api_client): assert res["version"] == "1.0" assert res["change_description"] == "Approved version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1128,7 +1175,7 @@ def test_approving_an_odm_form(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { @@ -1171,20 +1218,46 @@ def test_inactivating_a_specific_odm_form(api_client): assert res["version"] == "2.0" assert res["change_description"] == "Inactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1212,7 +1285,7 @@ def test_inactivating_a_specific_odm_form(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { @@ -1255,20 +1328,46 @@ def test_reactivating_a_specific_odm_form(api_client): assert res["version"] == "3.0" assert res["change_description"] == "Reactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1296,7 +1395,7 @@ def test_reactivating_a_specific_odm_form(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { @@ -1339,20 +1438,46 @@ def test_creating_a_new_odm_form_version(api_client): assert res["version"] == "3.1" assert res["change_description"] == "New draft created" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1380,7 +1505,7 @@ def test_creating_a_new_odm_form_version(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { @@ -1412,13 +1537,11 @@ def test_create_a_new_odm_form_for_deleting_it(api_client): "oid": "oid2", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ], "aliases": [], @@ -1440,13 +1563,11 @@ def test_create_a_new_odm_form_for_deleting_it(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ] assert res["aliases"] == [] @@ -1470,21 +1591,23 @@ def test_creating_a_new_odm_form_with_relations(api_client): "oid": "string", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string1"}, + {"text_type": "Description", "language": "dan", "text": "string2"}, { - "name": "string1", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string1", - "instruction": "string1", - "sponsor_instruction": "string1", + "text": "string1", }, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "dan", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string1"}, + {"text_type": "osb:DesignNotes", "language": "dan", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string1"}, + {"text_type": "osb:DisplayText", "language": "dan", "text": "string2"}, ], "aliases": [], } @@ -1505,21 +1628,23 @@ def test_creating_a_new_odm_form_with_relations(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string1"}, + {"text_type": "Description", "language": "dan", "text": "string2"}, { - "name": "string1", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string1", - "instruction": "string1", - "sponsor_instruction": "string1", + "text": "string1", }, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "dan", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string1"}, + {"text_type": "osb:DesignNotes", "language": "dan", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string1"}, + {"text_type": "osb:DisplayText", "language": "dan", "text": "string2"}, ] assert res["aliases"] == [] assert res["item_groups"] == [] @@ -1537,23 +1662,34 @@ def test_updating_an_existing_odm_form_with_relations(api_client): "sdtm_version": "0.1", "repeating": "Yes", "change_description": "repeating changed to Yes", - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string2"}, + {"text_type": "Description", "language": "ara", "text": "string3"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, { - "name": "string3", - "language": "ara", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DesignNotes", "language": "ara", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "ara", "text": "string3"}, ], "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [ + {"uid": "odm_vendor_element3", "value": "value"}, + ], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute7", "value": "value"}, + ], + "vendor_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"}, + ], } response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) @@ -1572,21 +1708,23 @@ def test_updating_an_existing_odm_form_with_relations(api_client): assert res["version"] == "3.2" assert res["change_description"] == "repeating changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string2"}, + {"text_type": "Description", "language": "ara", "text": "string3"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, { - "name": "string3", - "language": "ara", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DesignNotes", "language": "ara", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "ara", "text": "string3"}, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["item_groups"] == [ @@ -1613,17 +1751,17 @@ def test_updating_an_existing_odm_form_with_relations(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { @@ -1704,6 +1842,7 @@ def test_add_the_odm_form_to_the_odm_study_event(api_client): assert res["forms"] == [ { "uid": "OdmForm_000001", + "oid": "oid1", "name": "name1", "version": "3.2", "order_number": 1, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms_negative.py index b332315d..d52833a8 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_forms_negative.py @@ -45,20 +45,46 @@ def test_create_a_new_odm_form(api_client): "oid": "oid1", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -80,20 +106,46 @@ def test_create_a_new_odm_form(api_client): assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" assert res["sdtm_version"] == "0.1" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -107,10 +159,61 @@ def test_create_a_new_odm_form(api_client): def test_cannot_add_odm_vendor_attribute_with_an_invalid_value_to_an_odm_form( api_client, ): - data = [{"uid": "odm_vendor_attribute3", "value": "3423"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute3", "value": "3423"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -128,10 +231,61 @@ def test_cannot_add_odm_vendor_attribute_with_an_invalid_value_to_an_odm_form( def test_cannot_add_odm_vendor_element_attribute_with_an_invalid_value_to_an_odm_form( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "3423"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-element-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "3423"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -147,12 +301,65 @@ def test_cannot_add_odm_vendor_element_attribute_with_an_invalid_value_to_an_odm def test_add_odm_vendor_element_to_an_odm_form(api_client): - data = [{"uid": "odm_vendor_element1", "value": "value1"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-elements", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value1"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "valueOne"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) - assert_response_status_code(response, 201) + assert_response_status_code(response, 200) res = response.json() @@ -167,73 +374,52 @@ def test_add_odm_vendor_element_to_an_odm_form(api_client): assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" assert res["sdtm_version"] == "0.1" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["item_groups"] == [] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_add_odm_vendor_element_attribute_to_an_odm_form(api_client): - data = [{"uid": "odm_vendor_attribute1", "value": "valueOne"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-element-attributes", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmForm_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "No" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.1" - assert res["change_description"] == "Initial version" - assert res["author_username"] == "unknown-user@example.com" - assert res["sdtm_version"] == "0.1" - assert res["descriptions"] == [ { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["item_groups"] == [] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -256,20 +442,46 @@ def test_cannot_create_a_new_odm_form_with_same_properties(api_client): "oid": "oid1", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -294,13 +506,11 @@ def test_cannot_create_a_new_odm_form_without_an_english_description(api_client) "oid": "oid1", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [ + "translated_texts": [ { - "name": "name - non-eng", + "text_type": "Description", "language": "DAN", - "description": "description - non-eng", - "instruction": "instruction - non-eng", - "sponsor_instruction": "sponsor_instruction - non-eng", + "text": "text - non-eng", } ], "aliases": [], @@ -313,7 +523,8 @@ def test_cannot_create_a_new_odm_form_without_an_english_description(api_client) assert res["type"] == "ValidationException" assert ( - res["message"] == "At least one description must be in English ('eng' or 'en')." + res["message"] + == "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." ) @@ -356,10 +567,63 @@ def test_cannot_reactivate_an_odm_form_that_is_not_retired(api_client): def test_cannot_override_odm_vendor_element_that_has_attributes_connected_this_odm_form( api_client, ): - data = [{"uid": "odm_vendor_element2", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-elements?override=true", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [{"uid": "odm_vendor_element2", "value": "value"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "valueOne"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -375,10 +639,61 @@ def test_cannot_override_odm_vendor_element_that_has_attributes_connected_this_o def test_cannot_add_odm_vendor_element_attribute_to_an_odm_form_as_an_odm_vendor_attribute( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -394,12 +709,63 @@ def test_cannot_add_odm_vendor_element_attribute_to_an_odm_form_as_an_odm_vendor def test_cannot_add_odm_vendor_attribute_to_an_odm_form_as_an_odm_vendor_element_attribute( api_client, ): - data = [{"uid": "odm_vendor_attribute3", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-element-attributes", json=data - ) - - assert_response_status_code(response, 400) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value1"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) res = response.json() @@ -441,10 +807,61 @@ def test_cannot_add_odm_item_groups_with_an_invalid_value_to_an_odm_form(api_cli def test_cannot_add_a_non_compatible_odm_vendor_attribute_to_an_odm_form(api_client): - data = [{"uid": "odm_vendor_attribute5", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute5", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -460,10 +877,61 @@ def test_cannot_add_a_non_compatible_odm_vendor_attribute_to_an_odm_form(api_cli def test_cannot_add_a_non_compatible_odm_vendor_element_to_an_odm_form(api_client): - data = [{"uid": "odm_vendor_element4", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-elements", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [{"uid": "odm_vendor_element4", "value": "value"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -526,26 +994,52 @@ def test_approve_odm_form(api_client): assert res["change_description"] == "Approved version" assert res["author_username"] == "unknown-user@example.com" assert res["sdtm_version"] == "0.1" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["item_groups"] == [] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -579,26 +1073,52 @@ def test_inactivate_odm_form(api_client): assert res["change_description"] == "Inactivated version" assert res["author_username"] == "unknown-user@example.com" assert res["sdtm_version"] == "0.1" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["item_groups"] == [] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -642,10 +1162,61 @@ def test_cannot_add_odm_item_groups_to_an_odm_form_that_is_in_retired_status( def test_cannot_add_odm_vendor_element_to_an_odm_form_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_element1", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-elements", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -658,10 +1229,61 @@ def test_cannot_add_odm_vendor_element_to_an_odm_form_that_is_in_retired_status( def test_cannot_add_odm_vendor_attribute_to_an_odm_form_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -674,10 +1296,63 @@ def test_cannot_add_odm_vendor_attribute_to_an_odm_form_that_is_in_retired_statu def test_cannot_add_odm_vendor_element_attribute_to_an_odm_form_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-element-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "vendor_elements": [], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "value"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) assert_response_status_code(response, 400) @@ -685,3 +1360,43 @@ def test_cannot_add_odm_vendor_element_attribute_to_an_odm_form_that_is_in_retir assert res["type"] == "BusinessLogicException" assert res["message"] == "ODM element is not in Draft." + + +@pytest.mark.parametrize( + "text_type", + [ + pytest.param("Description"), + pytest.param("Question"), + pytest.param("osb:DesignNotes"), + pytest.param("osb:CompletionInstructions"), + pytest.param("osb:DisplayText"), + ], +) +def test_cannot_add_duplicate_translated_texts(api_client, text_type: str): + data = { + "library_name": "Sponsor", + "name": "testing", + "oid": "testing", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [ + { + "text_type": text_type, + "language": "eng", + "text": str(r), + } + for r in range(2) + ], + "aliases": [], + } + response = api_client.post("concepts/odms/forms", json=data) + + assert_response_status_code(response, 400) + + res = response.json() + + assert res["type"] == "ValidationException" + assert ( + res["message"] + == f"Duplicate Translated Text found for text_type '{text_type}' and language 'eng'." + ) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups.py index 3d460de8..78d756c3 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups.py @@ -86,24 +86,55 @@ def test_creating_a_new_odm_item_group(api_client): "origin": "origin1", "purpose": "purpose1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], "sdtm_domain_uids": ["domain001"], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute2", "value": "value"} + ], + "vendor_attributes": [{"uid": "odm_vendor_attribute4", "value": "value"}], } response = api_client.post("concepts/odms/item-groups", json=data) @@ -126,20 +157,46 @@ def test_creating_a_new_odm_item_group(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -158,9 +215,29 @@ def test_creating_a_new_odm_item_group(api_client): } ] assert res["items"] == [] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -186,20 +263,46 @@ def test_getting_non_empty_list_of_odm_item_groups(api_client): assert res["items"][0]["version"] == "0.1" assert res["items"][0]["change_description"] == "Initial version" assert res["items"][0]["author_username"] == "unknown-user@example.com" - assert res["items"][0]["descriptions"] == [ + assert res["items"][0]["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["items"][0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -218,9 +321,29 @@ def test_getting_non_empty_list_of_odm_item_groups(api_client): } ] assert res["items"][0]["items"] == [] - assert res["items"][0]["vendor_elements"] == [] - assert res["items"][0]["vendor_attributes"] == [] - assert res["items"][0]["vendor_element_attributes"] == [] + assert res["items"][0]["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["items"][0]["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["items"][0]["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["items"][0]["possible_actions"] == ["approve", "delete", "edit"] @@ -256,20 +379,46 @@ def test_getting_a_specific_odm_item_group(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -288,9 +437,29 @@ def test_getting_a_specific_odm_item_group(api_client): } ] assert res["items"] == [] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -316,20 +485,46 @@ def test_getting_versions_of_a_specific_odm_item_group(api_client): assert res[0]["version"] == "0.1" assert res[0]["change_description"] == "Initial version" assert res[0]["author_username"] == "unknown-user@example.com" - assert res[0]["descriptions"] == [ + assert res[0]["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res[0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -348,9 +543,29 @@ def test_getting_versions_of_a_specific_odm_item_group(api_client): } ] assert res[0]["items"] == [] - assert res[0]["vendor_elements"] == [] - assert res[0]["vendor_attributes"] == [] - assert res[0]["vendor_element_attributes"] == [] + assert res[0]["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res[0]["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res[0]["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res[0]["possible_actions"] == ["approve", "delete", "edit"] @@ -366,24 +581,59 @@ def test_updating_an_existing_odm_item_group(api_client): "purpose": "purpose1", "comment": "comment1", "change_description": "repeating and is_reference_data changed to Yes", - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], "sdtm_domain_uids": ["domain001"], + "vendor_elements": [ + {"uid": "odm_vendor_element3", "value": "value"}, + ], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute7", "value": "value"}, + ], + "vendor_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"}, + ], } response = api_client.patch( "concepts/odms/item-groups/OdmItemGroup_000001", json=data @@ -408,20 +658,46 @@ def test_updating_an_existing_odm_item_group(api_client): assert res["version"] == "0.2" assert res["change_description"] == "repeating and is_reference_data changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -440,9 +716,29 @@ def test_updating_an_existing_odm_item_group(api_client): } ] assert res["items"] == [] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "data_type": "string", + "name": "nameThree", + "uid": "odm_vendor_attribute3", + "value": "value", + "value_regex": "^[a-zA-Z]+$", + "vendor_namespace_uid": "odm_vendor_namespace1", + }, + ] + assert res["vendor_element_attributes"] == [ + { + "data_type": "string", + "name": "nameSeven", + "uid": "odm_vendor_attribute7", + "value": "value", + "value_regex": None, + "vendor_element_uid": "odm_vendor_element3", + }, + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -470,26 +766,52 @@ def test_getting_a_specific_odm_item_group_in_specific_version(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ { - "codelist_name": "SDTM Domain Abbreviation", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ] + assert res["aliases"] == [{"context": "context1", "name": "name1"}] + assert res["sdtm_domains"] == [ + { + "codelist_name": "SDTM Domain Abbreviation", "codelist_submission_value": "DOMAIN", "codelist_uid": "C66734", "date_conflict": False, @@ -548,224 +870,46 @@ def test_adding_odm_items_to_a_specific_odm_item_group(api_client): assert res["version"] == "0.2" assert res["change_description"] == "repeating and is_reference_data changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text_type": "Description", + "language": "dan", + "text": "description3", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ - { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [ - { - "uid": "odm_item1", - "oid": "oid1", - "name": "name1", - "version": "1.0", - "order_number": 1, - "mandatory": "Yes", - "key_sequence": "key_sequence1", - "method_oid": "method_oid1", - "imputation_method_oid": "imputation_method_oid1", - "role": "role1", - "role_codelist_oid": "role_codelist_oid1", - "collection_exception_condition_oid": "collection_exception_condition_oid1", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_items_from_a_specific_odm_item_group(api_client): - data = [ - { - "uid": "odm_item2", - "order_number": 2, - "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/items?override=true", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "Yes" - assert res["is_reference_data"] == "Yes" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating and is_reference_data changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ { - "name": "name2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", }, { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [ + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, { - "uid": "odm_item2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_element_to_a_specific_odm_item_group(api_client): - data = [{"uid": "odm_vendor_element2", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-elements", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "Yes" - assert res["is_reference_data"] == "Yes" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating and is_reference_data changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, { - "name": "name2", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "name2", }, { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -785,520 +929,18 @@ def test_adding_odm_vendor_element_to_a_specific_odm_item_group(api_client): ] assert res["items"] == [ { - "uid": "odm_item2", - "oid": "oid2", - "name": "name2", + "uid": "odm_item1", + "oid": "oid1", + "name": "name1", "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element2", "name": "nameTwo", "value": "value"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_element_from_a_specific_odm_item_group(api_client): - data = [{"uid": "odm_vendor_element1", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-elements?override=true", - json=data, - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "Yes" - assert res["is_reference_data"] == "Yes" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating and is_reference_data changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ - { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [ - { - "uid": "odm_item2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_attribute_to_a_specific_odm_item_group(api_client): - data = [{"uid": "odm_vendor_attribute3", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-attributes", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "Yes" - assert res["is_reference_data"] == "Yes" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating and is_reference_data changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ - { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [ - { - "uid": "odm_item2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_attribute_from_a_specific_odm_item_group(api_client): - data = [{"uid": "odm_vendor_attribute4", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-attributes?override=true", - json=data, - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "Yes" - assert res["is_reference_data"] == "Yes" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating and is_reference_data changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ - { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [ - { - "uid": "odm_item2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute4", - "name": "nameFour", - "data_type": "string", - "value_regex": None, - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_element_attribute_to_a_specific_odm_item_group(api_client): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-element-attributes", - json=data, - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "Yes" - assert res["is_reference_data"] == "Yes" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating and is_reference_data changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ - { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [ - { - "uid": "odm_item2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, - "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", - "vendor": { - "attributes": [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "Yes", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - }, - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute4", - "name": "nameFour", - "data_type": "string", - "value_regex": None, - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [ - { - "uid": "odm_vendor_attribute1", - "name": "nameOne", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "value", - "vendor_element_uid": "odm_vendor_element1", - } - ] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item_group( - api_client, -): - data = [{"uid": "odm_vendor_attribute2", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-element-attributes?override=true", - json=data, - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["name"] == "name1" - assert res["library_name"] == "Sponsor" - assert res["oid"] == "oid1" - assert res["repeating"] == "Yes" - assert res["is_reference_data"] == "Yes" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "repeating and is_reference_data changed to Yes" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ - { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [ - { - "uid": "odm_item2", - "oid": "oid2", - "name": "name2", - "version": "1.0", - "order_number": 2, + "order_number": 1, "mandatory": "Yes", - "key_sequence": "key_sequence2", - "method_oid": "method_oid2", - "imputation_method_oid": "imputation_method_oid2", - "role": "role2", - "role_codelist_oid": "role_codelist_oid2", - "collection_exception_condition_oid": "collection_exception_condition_oid2", + "key_sequence": "key_sequence1", + "method_oid": "method_oid1", + "imputation_method_oid": "imputation_method_oid1", + "role": "role1", + "role_codelist_oid": "role_codelist_oid1", + "collection_exception_condition_oid": "collection_exception_condition_oid1", "vendor": { "attributes": [ { @@ -1314,41 +956,59 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item_group( } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute2", - "name": "nameTwo", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", - "vendor_element_uid": "odm_vendor_element1", - } + "value_regex": None, + "vendor_element_uid": "odm_vendor_element3", + }, ] assert res["possible_actions"] == ["approve", "delete", "edit"] -def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item_group1( - api_client, -): - data = { - "elements": [{"uid": "odm_vendor_element3", "value": "value"}], - "element_attributes": [{"uid": "odm_vendor_attribute7", "value": "value"}], - "attributes": [{"uid": "odm_vendor_attribute4", "value": "value"}], - } +def test_overriding_odm_items_from_a_specific_odm_item_group(api_client): + data = [ + { + "uid": "odm_item2", + "order_number": 2, + "mandatory": "Yes", + "key_sequence": "key_sequence2", + "method_oid": "method_oid2", + "imputation_method_oid": "imputation_method_oid2", + "role": "role2", + "role_codelist_oid": "role_codelist_oid2", + "collection_exception_condition_oid": "collection_exception_condition_oid2", + "vendor": { + "attributes": [ + { + "uid": "odm_vendor_attribute3", + "name": "nameThree", + "data_type": "string", + "value_regex": "^[a-zA-Z]+$", + "value": "Yes", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + }, + } + ] response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendors", json=data + "concepts/odms/item-groups/OdmItemGroup_000001/items?override=true", json=data ) assert_response_status_code(response, 201) @@ -1370,20 +1030,46 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item_group1 assert res["version"] == "0.2" assert res["change_description"] == "repeating and is_reference_data changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1430,27 +1116,27 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item_group1 } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -1479,20 +1165,46 @@ def test_approving_an_odm_item_group(api_client): assert res["version"] == "1.0" assert res["change_description"] == "Approved version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1539,27 +1251,27 @@ def test_approving_an_odm_item_group(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["inactivate", "new_version"] @@ -1588,20 +1300,46 @@ def test_inactivating_a_specific_odm_item_group(api_client): assert res["version"] == "2.0" assert res["change_description"] == "Inactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1648,27 +1386,27 @@ def test_inactivating_a_specific_odm_item_group(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["delete", "reactivate"] @@ -1697,20 +1435,46 @@ def test_reactivating_a_specific_odm_item_group(api_client): assert res["version"] == "3.0" assert res["change_description"] == "Reactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1757,27 +1521,27 @@ def test_reactivating_a_specific_odm_item_group(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["inactivate", "new_version"] @@ -1804,20 +1568,46 @@ def test_creating_a_new_odm_item_group_version(api_client): assert res["version"] == "3.1" assert res["change_description"] == "New draft created" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1864,27 +1654,27 @@ def test_creating_a_new_odm_item_group_version(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["approve", "edit"] @@ -1900,13 +1690,11 @@ def test_create_a_new_odm_item_group_for_deleting_it(api_client): "origin": "origin1", "purpose": "purpose1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ], "aliases": [], @@ -1933,13 +1721,11 @@ def test_create_a_new_odm_item_group_for_deleting_it(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ] assert res["aliases"] == [] @@ -1968,21 +1754,23 @@ def test_creating_a_new_odm_item_group_with_relations(api_client): "origin": "string", "purpose": "string", "comment": "string", - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string1"}, + {"text_type": "Description", "language": "dan", "text": "string2"}, { - "name": "string1", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string1", - "instruction": "string1", - "sponsor_instruction": "string1", + "text": "string1", }, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "dan", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string1"}, + {"text_type": "osb:DesignNotes", "language": "dan", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string1"}, + {"text_type": "osb:DisplayText", "language": "dan", "text": "string2"}, ], "aliases": [], "sdtm_domain_uids": [], @@ -2008,21 +1796,23 @@ def test_creating_a_new_odm_item_group_with_relations(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string1"}, + {"text_type": "Description", "language": "dan", "text": "string2"}, { - "name": "string1", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string1", - "instruction": "string1", - "sponsor_instruction": "string1", + "text": "string1", }, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "dan", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string1"}, + {"text_type": "osb:DesignNotes", "language": "dan", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string1"}, + {"text_type": "osb:DisplayText", "language": "dan", "text": "string2"}, ] assert res["aliases"] == [] assert res["sdtm_domains"] == [] @@ -2045,24 +1835,35 @@ def test_updating_an_existing_odm_item_group_with_relations(api_client): "purpose": "purpose1", "comment": "comment1", "change_description": "repeating and is_reference_data changed to Yes", - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string2"}, + {"text_type": "Description", "language": "ara", "text": "string3"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, { - "name": "string3", - "language": "ara", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DesignNotes", "language": "ara", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "ara", "text": "string3"}, ], "aliases": [{"context": "context1", "name": "name1"}], "sdtm_domain_uids": ["domain001"], + "vendor_elements": [ + {"uid": "odm_vendor_element3", "value": "value"}, + ], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute7", "value": "value"}, + ], + "vendor_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"}, + ], } response = api_client.patch( "concepts/odms/item-groups/OdmItemGroup_000001", json=data @@ -2087,21 +1888,23 @@ def test_updating_an_existing_odm_item_group_with_relations(api_client): assert res["version"] == "3.2" assert res["change_description"] == "repeating and is_reference_data changed to Yes" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string2"}, + {"text_type": "Description", "language": "ara", "text": "string3"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, { - "name": "string3", - "language": "ara", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DesignNotes", "language": "ara", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "ara", "text": "string3"}, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["sdtm_domains"] == [ @@ -2147,27 +1950,27 @@ def test_updating_an_existing_odm_item_group_with_relations(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["approve", "edit"] @@ -2179,7 +1982,7 @@ def test_create_a_new_odm_form_with_relation_to_odm_item_group(api_client): "oid": "oid1", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [], + "translated_texts": [], "aliases": [], } response = api_client.post("concepts/odms/forms", json=data) @@ -2199,7 +2002,7 @@ def test_create_a_new_odm_form_with_relation_to_odm_item_group(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [] + assert res["translated_texts"] == [] assert res["aliases"] == [] assert res["item_groups"] == [] assert res["vendor_elements"] == [] @@ -2248,7 +2051,7 @@ def test_add_the_odm_item_group_to_the_odm_form(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [] + assert res["translated_texts"] == [] assert res["aliases"] == [] assert res["item_groups"] == [ { diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups_negative.py index 1eeedb7c..633e5914 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_item_groups_negative.py @@ -74,20 +74,46 @@ def test_create_a_new_odm_item_group(api_client): "origin": "origin1", "purpose": "purpose1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -114,20 +140,46 @@ def test_create_a_new_odm_item_group(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", }, { - "name": "name3", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -155,9 +207,67 @@ def test_create_a_new_odm_item_group(api_client): def test_cannot_add_odm_vendor_attribute_with_an_invalid_value_to_an_odm_item_group( api_client, ): - data = [{"uid": "odm_vendor_attribute3", "value": "3423"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-attributes", json=data + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute3", "value": "3423"}], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -176,9 +286,67 @@ def test_cannot_add_odm_vendor_attribute_with_an_invalid_value_to_an_odm_item_gr def test_cannot_add_a_non_compatible_odm_vendor_attribute_to_an_odm_item_group( api_client, ): - data = [{"uid": "odm_vendor_attribute5", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-attributes", json=data + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute5", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -197,9 +365,67 @@ def test_cannot_add_a_non_compatible_odm_vendor_attribute_to_an_odm_item_group( def test_cannot_add_a_non_compatible_odm_vendor_element_to_an_odm_item_group( api_client, ): - data = [{"uid": "odm_vendor_element4", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-elements", json=data + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [{"uid": "odm_vendor_element4", "value": "value"}], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute3", "value": "3423"}], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -252,10 +478,67 @@ def test_cannot_add_odm_items_with_non_compatible_odm_vendor_attribute_to_a_spec def test_cannot_add_odm_vendor_element_attribute_with_an_invalid_value_to_an_odm_item_group( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "3423"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-element-attributes", - json=data, + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "3423"}], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -272,12 +555,72 @@ def test_cannot_add_odm_vendor_element_attribute_with_an_invalid_value_to_an_odm def test_add_odm_vendor_element_to_an_odm_item_group(api_client): - data = [{"uid": "odm_vendor_element1", "value": "value1"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-elements", json=data + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value1"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "valueOne"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) - assert_response_status_code(response, 201) + assert_response_status_code(response, 200) res = response.json() @@ -296,86 +639,46 @@ def test_add_odm_vendor_element_to_an_odm_item_group(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["sdtm_domains"] == [ { - "codelist_name": "SDTM Domain Abbreviation", - "codelist_submission_value": "DOMAIN", - "codelist_uid": "C66734", - "date_conflict": False, - "order": 1, - "queried_effective_date": None, - "submission_value": "XX", - "preferred_term": "test", - "term_name": "domain", - "term_uid": "domain001", - } - ] - assert res["items"] == [] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_add_odm_vendor_element_attribute_to_an_odm_item_group(api_client): - data = [{"uid": "odm_vendor_attribute1", "value": "valueOne"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-element-attributes", - json=data, - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItemGroup_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["repeating"] == "No" - assert res["is_reference_data"] == "No" - assert res["sas_dataset_name"] == "sas_dataset_name1" - assert res["origin"] == "origin1" - assert res["purpose"] == "purpose1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.1" - assert res["change_description"] == "Initial version" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -395,7 +698,7 @@ def test_add_odm_vendor_element_attribute_to_an_odm_item_group(api_client): ] assert res["items"] == [] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -422,20 +725,46 @@ def test_cannot_create_a_new_odm_item_group_with_same_properties(api_client): "origin": "origin1", "purpose": "purpose1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -467,7 +796,7 @@ def test_cannot_create_an_odm_item_group_connected_to_non_existent_sdtm_domain( "origin": "origin1", "purpose": "purpose1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "sdtm_domain_uids": ["wrong_uid"], } @@ -495,14 +824,12 @@ def test_cannot_create_a_new_odm_item_group_without_an_english_description(api_c "origin": "origin1", "purpose": "purpose1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name - non-eng", + "text_type": "Description", "language": "DAN", - "description": "description - non-eng", - "instruction": "instruction - non-eng", - "sponsor_instruction": "sponsor_instruction - non-eng", - } + "text": "description - non-eng", + }, ], "aliases": [], "sdtm_domain_uids": [], @@ -515,7 +842,8 @@ def test_cannot_create_a_new_odm_item_group_without_an_english_description(api_c assert res["type"] == "ValidationException" assert ( - res["message"] == "At least one description must be in English ('eng' or 'en')." + res["message"] + == "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." ) @@ -562,10 +890,69 @@ def test_cannot_reactivate_an_odm_item_group_that_is_not_retired(api_client): def test_cannot_override_odm_vendor_element_that_has_attributes_connected_this_odm_item_group( api_client, ): - data = [{"uid": "odm_vendor_element2", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-elements?override=true", - json=data, + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [{"uid": "odm_vendor_element2", "value": "value"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "valueOne"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -582,9 +969,67 @@ def test_cannot_override_odm_vendor_element_that_has_attributes_connected_this_o def test_cannot_add_odm_vendor_element_attribute_to_an_odm_item_group_as_an_odm_vendor_attribute( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-attributes", json=data + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -601,10 +1046,69 @@ def test_cannot_add_odm_vendor_element_attribute_to_an_odm_item_group_as_an_odm_ def test_cannot_add_odm_vendor_attribute_to_an_odm_item_group_as_an_odm_vendor_element_attribute( api_client, ): - data = [{"uid": "odm_vendor_attribute3", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-element-attributes", - json=data, + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value1"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -679,20 +1183,46 @@ def test_approve_odm_item_group(api_client): assert res["version"] == "1.0" assert res["change_description"] == "Approved version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -712,7 +1242,7 @@ def test_approve_odm_item_group(api_client): ] assert res["items"] == [] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -752,20 +1282,46 @@ def test_inactivate_odm_item_group(api_client): assert res["version"] == "2.0" assert res["change_description"] == "Inactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -785,7 +1341,7 @@ def test_inactivate_odm_item_group(api_client): ] assert res["items"] == [] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -836,9 +1392,67 @@ def test_cannot_add_odm_items_to_an_odm_item_group_that_is_in_retired_status( def test_cannot_add_odm_vendor_element_to_an_odm_item_group_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_element1", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-elements", json=data + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -852,9 +1466,67 @@ def test_cannot_add_odm_vendor_element_to_an_odm_item_group_that_is_in_retired_s def test_cannot_add_odm_vendor_attribute_to_an_odm_item_group_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-attributes", json=data + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -868,10 +1540,69 @@ def test_cannot_add_odm_vendor_attribute_to_an_odm_item_group_that_is_in_retired def test_cannot_add_odm_vendor_element_attribute_to_an_odm_item_group_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/item-groups/OdmItemGroup_000001/vendor-element-attributes", - json=data, + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "sas_dataset_name1", + "origin": "origin1", + "purpose": "purpose1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "sdtm_domain_uids": ["domain001"], + "vendor_elements": [], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "value"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch( + "concepts/odms/item-groups/OdmItemGroup_000001", json=data ) assert_response_status_code(response, 400) @@ -880,3 +1611,48 @@ def test_cannot_add_odm_vendor_element_attribute_to_an_odm_item_group_that_is_in assert res["type"] == "BusinessLogicException" assert res["message"] == "ODM element is not in Draft." + + +@pytest.mark.parametrize( + "text_type", + [ + pytest.param("Description"), + pytest.param("Question"), + pytest.param("osb:DesignNotes"), + pytest.param("osb:CompletionInstructions"), + pytest.param("osb:DisplayText"), + ], +) +def test_cannot_add_duplicate_translated_texts(api_client, text_type: str): + data = { + "library_name": "Sponsor", + "name": "testin", + "oid": "testin", + "repeating": "No", + "is_reference_data": "No", + "sas_dataset_name": "testing", + "origin": "testing", + "purpose": "testing", + "comment": "testing", + "translated_texts": [ + { + "text_type": text_type, + "language": "eng", + "text": str(r), + } + for r in range(2) + ], + "aliases": [], + "sdtm_domain_uids": [], + } + response = api_client.post("concepts/odms/item-groups", json=data) + + assert_response_status_code(response, 400) + + res = response.json() + + assert res["type"] == "ValidationException" + assert ( + res["message"] + == f"Duplicate Translated Text found for text_type '{text_type}' and language 'eng'." + ) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items.py index 59c35b3f..7e057915 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items.py @@ -61,27 +61,53 @@ def test_creating_a_new_odm_item(api_client): "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], "unit_definitions": [ {"uid": "unit_definition_root1", "mandatory": False, "order": 1} ], - "codelist_uid": "editable_cr", + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, "terms": [ { "uid": "term_root_final", @@ -91,6 +117,11 @@ def test_creating_a_new_odm_item(api_client): "version": "1.0", } ], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute2", "value": "value"} + ], + "vendor_attributes": [{"uid": "odm_vendor_attribute4", "value": "value"}], } response = api_client.post("concepts/odms/items", json=data) @@ -115,20 +146,46 @@ def test_creating_a_new_odm_item(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -136,6 +193,7 @@ def test_creating_a_new_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -149,8 +207,10 @@ def test_creating_a_new_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -163,9 +223,29 @@ def test_creating_a_new_odm_item(api_client): "version": "1.0", } ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -193,20 +273,46 @@ def test_getting_non_empty_list_of_odm_items(api_client): assert res["items"][0]["version"] == "0.1" assert res["items"][0]["change_description"] == "Initial version" assert res["items"][0]["author_username"] == "unknown-user@example.com" - assert res["items"][0]["descriptions"] == [ + assert res["items"][0]["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["items"][0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -214,6 +320,7 @@ def test_getting_non_empty_list_of_odm_items(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -227,8 +334,10 @@ def test_getting_non_empty_list_of_odm_items(api_client): assert res["items"][0]["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["items"][0]["terms"] == [ { @@ -241,9 +350,29 @@ def test_getting_non_empty_list_of_odm_items(api_client): "version": "1.0", } ] - assert res["items"][0]["vendor_elements"] == [] - assert res["items"][0]["vendor_attributes"] == [] - assert res["items"][0]["vendor_element_attributes"] == [] + assert res["items"][0]["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["items"][0]["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["items"][0]["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["items"][0]["possible_actions"] == ["approve", "delete", "edit"] @@ -281,20 +410,46 @@ def test_getting_a_specific_odm_item(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -302,6 +457,7 @@ def test_getting_a_specific_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -315,8 +471,10 @@ def test_getting_a_specific_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -329,9 +487,29 @@ def test_getting_a_specific_odm_item(api_client): "version": "1.0", } ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] + assert res["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -359,20 +537,46 @@ def test_getting_versions_of_a_specific_odm_item(api_client): assert res[0]["version"] == "0.1" assert res[0]["change_description"] == "Initial version" assert res[0]["author_username"] == "unknown-user@example.com" - assert res[0]["descriptions"] == [ + assert res[0]["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res[0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -380,6 +584,7 @@ def test_getting_versions_of_a_specific_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -393,8 +598,10 @@ def test_getting_versions_of_a_specific_odm_item(api_client): assert res[0]["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res[0]["terms"] == [ { @@ -407,9 +614,29 @@ def test_getting_versions_of_a_specific_odm_item(api_client): "version": "1.0", } ] - assert res[0]["vendor_elements"] == [] - assert res[0]["vendor_attributes"] == [] - assert res[0]["vendor_element_attributes"] == [] + assert res[0]["vendor_elements"] == [ + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value"} + ] + assert res[0]["vendor_attributes"] == [ + { + "uid": "odm_vendor_attribute4", + "name": "nameFour", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_namespace_uid": "odm_vendor_namespace1", + } + ] + assert res[0]["vendor_element_attributes"] == [ + { + "uid": "odm_vendor_attribute2", + "name": "nameTwo", + "value": "value", + "value_regex": None, + "data_type": "string", + "vendor_element_uid": "odm_vendor_element1", + } + ] assert res[0]["possible_actions"] == ["approve", "delete", "edit"] @@ -427,27 +654,53 @@ def test_updating_an_existing_odm_item(api_client): "origin": "origin1", "comment": "new comment", "change_description": "comment added", - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], "unit_definitions": [ {"uid": "unit_definition_root1", "mandatory": False, "order": 1} ], - "codelist_uid": "editable_cr", + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, "terms": [ { "uid": "term_root_final", @@ -457,621 +710,19 @@ def test_updating_an_existing_odm_item(api_client): "version": "1.0", } ], - } - response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) - - assert_response_status_code(response, 200) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 22 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "new comment" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "comment added" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ - { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ - { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": "display text", - "version": "1.0", - } - ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_getting_a_specific_odm_item_in_specific_version(api_client): - response = api_client.get("concepts/odms/items/OdmItem_000001?version=0.1") - - assert_response_status_code(response, 200) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 1 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "comment1" - assert res["end_date"] - assert res["status"] == "Draft" - assert res["version"] == "0.1" - assert res["change_description"] == "Initial version" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ - { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ - { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": "display text", - "version": "1.0", - } - ] - assert res["vendor_elements"] == [] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_element_to_a_specific_odm_item(api_client): - data = [{"uid": "odm_vendor_element2", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-elements", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 22 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "new comment" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "comment added" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ - { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ - { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": "display text", - "version": "1.0", - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element2", "name": "nameTwo", "value": "value"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_element_from_a_specific_odm_item(api_client): - data = [{"uid": "odm_vendor_element1", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-elements?override=true", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 22 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "new comment" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "comment added" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ - { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ - { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": "display text", - "version": "1.0", - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_attribute_to_a_specific_odm_item(api_client): - data = [{"uid": "odm_vendor_attribute3", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-attributes", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 22 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "new comment" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "comment added" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ - { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ - { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": "display text", - "version": "1.0", - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute3", - "name": "nameThree", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_attribute_from_a_specific_odm_item(api_client): - data = [{"uid": "odm_vendor_attribute4", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-attributes?override=true", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 22 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "new comment" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "comment added" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ - { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ - { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": "display text", - "version": "1.0", - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute4", - "name": "nameFour", - "data_type": "string", - "value_regex": None, - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_adding_odm_vendor_element_attribute_to_a_specific_odm_item(api_client): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-element-attributes", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 22 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "new comment" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "comment added" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ - { - "name": "name2", - "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", - }, - { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", - }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ - { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ - { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": "display text", - "version": "1.0", - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute4", - "name": "nameFour", - "data_type": "string", - "value_regex": None, - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [ - { - "uid": "odm_vendor_attribute1", - "name": "nameOne", - "data_type": "string", - "value_regex": "^[a-zA-Z]+$", - "value": "value", - "vendor_element_uid": "odm_vendor_element1", - } - ] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item(api_client): - data = [{"uid": "odm_vendor_attribute2", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-element-attributes?override=true", - json=data, - ) + "vendor_elements": [ + {"uid": "odm_vendor_element3", "value": "value"}, + ], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute7", "value": "value"}, + ], + "vendor_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"}, + ], + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) - assert_response_status_code(response, 201) + assert_response_status_code(response, 200) res = response.json() @@ -1092,20 +743,46 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item(api_cl assert res["version"] == "0.2" assert res["change_description"] == "comment added" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1113,6 +790,7 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item(api_cl { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1126,8 +804,10 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item(api_cl assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -1141,40 +821,35 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item(api_cl } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute2", - "name": "nameTwo", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", - "vendor_element_uid": "odm_vendor_element1", - } + "value_regex": None, + "vendor_element_uid": "odm_vendor_element3", + }, ] assert res["possible_actions"] == ["approve", "delete", "edit"] -def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item1(api_client): - data = { - "elements": [{"uid": "odm_vendor_element3", "value": "value"}], - "element_attributes": [{"uid": "odm_vendor_attribute7", "value": "value"}], - "attributes": [{"uid": "odm_vendor_attribute4", "value": "value"}], - } - response = api_client.post("concepts/odms/items/OdmItem_000001/vendors", json=data) +def test_getting_a_specific_odm_item_in_specific_version(api_client): + response = api_client.get("concepts/odms/items/OdmItem_000001?version=0.1") - assert_response_status_code(response, 201) + assert_response_status_code(response, 200) res = response.json() @@ -1184,31 +859,57 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item1(api_c assert res["oid"] == "oid1" assert res["prompt"] == "prompt1" assert res["datatype"] == "string" - assert res["length"] == 22 + assert res["length"] == 1 assert res["significant_digits"] == 11 assert res["sas_field_name"] == "sas_field_name1" assert res["sds_var_name"] == "sds_var_name1" assert res["origin"] == "origin1" - assert res["comment"] == "new comment" - assert res["end_date"] is None + assert res["comment"] == "comment1" + assert res["end_date"] assert res["status"] == "Draft" - assert res["version"] == "0.2" - assert res["change_description"] == "comment added" + assert res["version"] == "0.1" + assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1216,6 +917,7 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item1(api_c { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1229,8 +931,10 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item1(api_c assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -1243,29 +947,9 @@ def test_overriding_odm_vendor_element_attribute_from_a_specific_odm_item1(api_c "version": "1.0", } ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} - ] - assert res["vendor_attributes"] == [ - { - "uid": "odm_vendor_attribute4", - "name": "nameFour", - "data_type": "string", - "value_regex": None, - "value": "value", - "vendor_namespace_uid": "odm_vendor_namespace1", - } - ] - assert res["vendor_element_attributes"] == [ - { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", - "data_type": "string", - "value_regex": None, - "value": "value", - "vendor_element_uid": "odm_vendor_element3", - } - ] + assert res["vendor_elements"] == [] + assert res["vendor_attributes"] == [] + assert res["vendor_element_attributes"] == [] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -1293,20 +977,46 @@ def test_approving_an_odm_item(api_client): assert res["version"] == "1.0" assert res["change_description"] == "Approved version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1314,6 +1024,7 @@ def test_approving_an_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1327,8 +1038,10 @@ def test_approving_an_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -1342,27 +1055,27 @@ def test_approving_an_odm_item(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["inactivate", "new_version"] @@ -1391,20 +1104,46 @@ def test_inactivating_a_specific_odm_item(api_client): assert res["version"] == "2.0" assert res["change_description"] == "Inactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1412,6 +1151,7 @@ def test_inactivating_a_specific_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1425,8 +1165,10 @@ def test_inactivating_a_specific_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -1440,27 +1182,27 @@ def test_inactivating_a_specific_odm_item(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["delete", "reactivate"] @@ -1489,20 +1231,46 @@ def test_reactivating_a_specific_odm_item(api_client): assert res["version"] == "3.0" assert res["change_description"] == "Reactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1510,6 +1278,7 @@ def test_reactivating_a_specific_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1523,8 +1292,10 @@ def test_reactivating_a_specific_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -1538,27 +1309,27 @@ def test_reactivating_a_specific_odm_item(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["inactivate", "new_version"] @@ -1587,20 +1358,46 @@ def test_creating_a_new_odm_item_version(api_client): assert res["version"] == "3.1" assert res["change_description"] == "New draft created" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -1608,6 +1405,7 @@ def test_creating_a_new_odm_item_version(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1621,8 +1419,10 @@ def test_creating_a_new_odm_item_version(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -1636,27 +1436,27 @@ def test_creating_a_new_odm_item_version(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["approve", "edit"] @@ -1674,13 +1474,11 @@ def test_create_a_new_odm_item_for_deleting_it(api_client): "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name3 - delete", + "text_type": "Description", "language": "eng", - "description": "description3 - delete", - "instruction": "instruction3 - delete", - "sponsor_instruction": "sponsor_instruction3 - delete", + "text": "name3 - delete", } ], "aliases": [], @@ -1713,13 +1511,11 @@ def test_create_a_new_odm_item_for_deleting_it(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name3 - delete", + "text_type": "Description", "language": "eng", - "description": "description3 - delete", - "instruction": "instruction3 - delete", - "sponsor_instruction": "sponsor_instruction3 - delete", + "text": "name3 - delete", } ] assert res["aliases"] == [] @@ -1727,6 +1523,7 @@ def test_create_a_new_odm_item_for_deleting_it(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1764,21 +1561,23 @@ def test_creating_a_new_odm_item_with_relations(api_client): "sds_var_name": "string", "origin": "string", "comment": "string", - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string1"}, + {"text_type": "Description", "language": "dan", "text": "string2"}, { - "name": "string1", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string1", - "instruction": "string1", - "sponsor_instruction": "string1", + "text": "string1", }, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "dan", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string1"}, + {"text_type": "osb:DesignNotes", "language": "dan", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string1"}, + {"text_type": "osb:DisplayText", "language": "dan", "text": "string2"}, ], "aliases": [], "unit_definitions": [], @@ -1808,21 +1607,23 @@ def test_creating_a_new_odm_item_with_relations(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string1"}, + {"text_type": "Description", "language": "dan", "text": "string2"}, { - "name": "string1", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string1", - "instruction": "string1", - "sponsor_instruction": "string1", + "text": "string1", }, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "dan", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string1"}, + {"text_type": "osb:DesignNotes", "language": "dan", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string1"}, + {"text_type": "osb:DisplayText", "language": "dan", "text": "string2"}, ] assert res["aliases"] == [] assert res["unit_definitions"] == [] @@ -1848,28 +1649,39 @@ def test_updating_an_existing_odm_item_with_relations(api_client): "origin": "origin1", "comment": "new comment", "change_description": "comment added", - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string2"}, + {"text_type": "Description", "language": "ara", "text": "string3"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, { - "name": "string3", - "language": "ara", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DesignNotes", "language": "ara", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "ara", "text": "string3"}, ], "aliases": [{"context": "context1", "name": "name1"}], "unit_definitions": [ {"uid": "unit_definition_root1", "mandatory": False, "order": 1} ], - "codelist_uid": "editable_cr", + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, "terms": [{"uid": "term_root_final", "mandatory": True, "order": 1}], + "vendor_elements": [ + {"uid": "odm_vendor_element3", "value": "value"}, + ], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute7", "value": "value"}, + ], + "vendor_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"}, + ], } response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) @@ -1894,27 +1706,30 @@ def test_updating_an_existing_odm_item_with_relations(api_client): assert res["version"] == "3.2" assert res["change_description"] == "comment added" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string2"}, + {"text_type": "Description", "language": "ara", "text": "string3"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, { - "name": "string3", - "language": "ara", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DesignNotes", "language": "ara", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "ara", "text": "string3"}, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["unit_definitions"] == [ { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -1928,8 +1743,10 @@ def test_updating_an_existing_odm_item_with_relations(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -1943,27 +1760,27 @@ def test_updating_an_existing_odm_item_with_relations(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element3", "name": "nameThree", "value": "value"} + {"uid": "odm_vendor_element3", "name": "NameThree", "value": "value"} ] assert res["vendor_attributes"] == [ { - "uid": "odm_vendor_attribute4", - "name": "nameFour", "data_type": "string", - "value_regex": None, + "name": "nameThree", + "uid": "odm_vendor_attribute3", "value": "value", + "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - } + }, ] assert res["vendor_element_attributes"] == [ { - "uid": "odm_vendor_attribute7", - "name": "nameSeven", "data_type": "string", - "value_regex": None, + "name": "nameSeven", + "uid": "odm_vendor_attribute7", "value": "value", + "value_regex": None, "vendor_element_uid": "odm_vendor_element3", - } + }, ] assert res["possible_actions"] == ["approve", "edit"] @@ -1979,7 +1796,7 @@ def test_create_a_new_odm_item_group_with_relation_to_odm_item(api_client): "origin": "origin1", "purpose": "purpose1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "sdtm_domain_uids": [], } @@ -2004,7 +1821,7 @@ def test_create_a_new_odm_item_group_with_relation_to_odm_item(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [] + assert res["translated_texts"] == [] assert res["aliases"] == [] assert res["sdtm_domains"] == [] assert res["items"] == [] @@ -2054,7 +1871,7 @@ def test_add_the_odm_item_to_the_odm_item_group(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [] + assert res["translated_texts"] == [] assert res["aliases"] == [] assert res["sdtm_domains"] == [] assert res["items"] == [ @@ -2115,7 +1932,7 @@ def test_approve_the_odm_item_group(api_client): assert res["version"] == "1.0" assert res["change_description"] == "Approved version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [] + assert res["translated_texts"] == [] assert res["aliases"] == [] assert res["sdtm_domains"] == [] assert res["items"] == [ diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items_negative.py index 6be5a17e..72d58b62 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_items_negative.py @@ -51,27 +51,53 @@ def test_create_a_new_odm_item(api_client): "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], "unit_definitions": [ {"uid": "unit_definition_root1", "mandatory": False, "order": 1} ], - "codelist_uid": "editable_cr", + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, "terms": [ { "uid": "term_root_final", @@ -105,20 +131,46 @@ def test_create_a_new_odm_item(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -126,6 +178,7 @@ def test_create_a_new_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -139,8 +192,10 @@ def test_create_a_new_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -162,10 +217,80 @@ def test_create_a_new_odm_item(api_client): def test_cannot_add_odm_vendor_attribute_with_an_invalid_value_to_an_odm_item( api_client, ): - data = [{"uid": "odm_vendor_attribute3", "value": "3423"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute3", "value": "3423"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -181,10 +306,80 @@ def test_cannot_add_odm_vendor_attribute_with_an_invalid_value_to_an_odm_item( def test_cannot_add_a_non_compatible_odm_vendor_attribute_to_an_odm_item(api_client): - data = [{"uid": "odm_vendor_attribute5", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute5", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -200,10 +395,80 @@ def test_cannot_add_a_non_compatible_odm_vendor_attribute_to_an_odm_item(api_cli def test_cannot_add_a_non_compatible_odm_vendor_element_to_an_odm_item(api_client): - data = [{"uid": "odm_vendor_element4", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-elements", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [{"uid": "odm_vendor_element4", "value": "value"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -221,10 +486,80 @@ def test_cannot_add_a_non_compatible_odm_vendor_element_to_an_odm_item(api_clien def test_cannot_add_odm_vendor_element_attribute_with_an_invalid_value_to_an_odm_item( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "3423"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-element-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "3423"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -240,12 +575,84 @@ def test_cannot_add_odm_vendor_element_attribute_with_an_invalid_value_to_an_odm def test_add_odm_vendor_element_to_an_odm_item(api_client): - data = [{"uid": "odm_vendor_element1", "value": "value1"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-elements", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value1"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "valueOne"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) - assert_response_status_code(response, 201) + assert_response_status_code(response, 200) res = response.json() @@ -266,103 +673,46 @@ def test_add_odm_vendor_element_to_an_odm_item(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", - "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text_type": "Description", + "language": "dan", + "text": "description3", }, - ] - assert res["aliases"] == [{"context": "context1", "name": "name1"}] - assert res["unit_definitions"] == [ { - "uid": "unit_definition_root1", - "name": "name1", - "mandatory": False, - "order": 1, - "ucum": { - "term_uid": "term_root1_uid", - "name": "name1", - "dictionary_id": "dictionary_id1", - }, - "ct_units": [{"term_uid": "C25532_name1", "name": "name1"}], - } - ] - assert res["codelist"] == { - "uid": "editable_cr", - "name": "codelist attributes value1", - "submission_value": "codelist submission value1", - "preferred_term": "codelist preferred term", - } - assert res["terms"] == [ + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "term_uid": "term_root_final", - "name": "term_value_name1", - "mandatory": True, - "order": 1, - "submission_value": "submission_value_1", - "display_text": None, - "version": "1.0", - } - ] - assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} - ] - assert res["vendor_attributes"] == [] - assert res["vendor_element_attributes"] == [] - assert res["possible_actions"] == ["approve", "delete", "edit"] - - -def test_add_odm_vendor_element_attribute_to_an_odm_item(api_client): - data = [{"uid": "odm_vendor_attribute1", "value": "valueOne"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-element-attributes", json=data - ) - - assert_response_status_code(response, 201) - - res = response.json() - - assert res["uid"] == "OdmItem_000001" - assert res["library_name"] == "Sponsor" - assert res["name"] == "name1" - assert res["oid"] == "oid1" - assert res["prompt"] == "prompt1" - assert res["datatype"] == "string" - assert res["length"] == 11 - assert res["significant_digits"] == 11 - assert res["sas_field_name"] == "sas_field_name1" - assert res["sds_var_name"] == "sds_var_name1" - assert res["origin"] == "origin1" - assert res["comment"] == "comment1" - assert res["end_date"] is None - assert res["status"] == "Draft" - assert res["version"] == "0.1" - assert res["change_description"] == "Initial version" - assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -370,6 +720,7 @@ def test_add_odm_vendor_element_attribute_to_an_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -383,8 +734,10 @@ def test_add_odm_vendor_element_attribute_to_an_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -398,7 +751,7 @@ def test_add_odm_vendor_element_attribute_to_an_odm_item(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -427,27 +780,53 @@ def test_cannot_create_a_new_odm_item_with_same_properties(api_client): "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], "unit_definitions": [ {"uid": "unit_definition_root1", "mandatory": False, "order": 1} ], - "codelist_uid": "editable_cr", + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, "terms": [ { "uid": "term_root_final", @@ -484,7 +863,7 @@ def test_cannot_create_an_odm_item_connected_to_non_existent_concepts(api_client "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "unit_definitions": [{"uid": "wrong_uid"}], "codelist_uid": None, @@ -516,10 +895,10 @@ def test_cannot_create_an_odm_item_connected_to_non_existent_codelist(api_client "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "unit_definitions": [], - "codelist_uid": "wrong_uid", + "codelist": {"uid": "wrong_uid"}, "terms": [], } response = api_client.post("concepts/odms/items", json=data) @@ -550,7 +929,7 @@ def test_cannot_create_an_odm_item_connected_to_ct_terms_without_providing_a_cod "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "unit_definitions": [], "codelist_uid": None, @@ -581,10 +960,10 @@ def test_cannot_create_an_odm_item_connected_to_ct_terms_belonging_to_a_codelist "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [], + "translated_texts": [], "aliases": [], "unit_definitions": [], - "codelist_uid": "editable_cr", + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, "terms": [{"uid": "wrong_uid"}], } response = api_client.post("concepts/odms/items", json=data) @@ -613,13 +992,11 @@ def test_cannot_create_a_new_odm_item_without_an_english_description(api_client) "sds_var_name": "sds_var_name1", "origin": "origin1", "comment": "comment1", - "descriptions": [ + "translated_texts": [ { - "name": "name - non-eng", + "text_type": "Description", "language": "DAN", - "description": "description - non-eng", - "instruction": "instruction - non-eng", - "sponsor_instruction": "sponsor_instruction - non-eng", + "text": "text - non-eng", } ], "aliases": [], @@ -635,7 +1012,8 @@ def test_cannot_create_a_new_odm_item_without_an_english_description(api_client) assert res["type"] == "ValidationException" assert ( - res["message"] == "At least one description must be in English ('eng' or 'en')." + res["message"] + == "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." ) @@ -678,10 +1056,82 @@ def test_cannot_reactivate_an_odm_item_that_is_not_retired(api_client): def test_cannot_override_odm_vendor_element_that_has_attributes_connected_this_odm_item( api_client, ): - data = [{"uid": "odm_vendor_element2", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-elements?override=true", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [{"uid": "odm_vendor_element2", "value": "value"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "valueOne"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -697,10 +1147,80 @@ def test_cannot_override_odm_vendor_element_that_has_attributes_connected_this_o def test_cannot_add_odm_vendor_element_attribute_to_an_odm_item_as_an_odm_vendor_attribute( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -716,10 +1236,83 @@ def test_cannot_add_odm_vendor_element_attribute_to_an_odm_item_as_an_odm_vendor def test_cannot_add_odm_vendor_attribute_to_an_odm_item_as_an_odm_vendor_element_attribute( api_client, ): - data = [{"uid": "odm_vendor_attribute3", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-element-attributes", json=data - ) + data = {} + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value1"}], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute3", "value": "value"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -756,20 +1349,46 @@ def test_approve_odm_item(api_client): assert res["version"] == "1.0" assert res["change_description"] == "Approved version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -777,6 +1396,7 @@ def test_approve_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -790,8 +1410,10 @@ def test_approve_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -805,7 +1427,7 @@ def test_approve_odm_item(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -845,20 +1467,46 @@ def test_inactivate_odm_item(api_client): assert res["version"] == "2.0" assert res["change_description"] == "Inactivated version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -866,6 +1514,7 @@ def test_inactivate_odm_item(api_client): { "uid": "unit_definition_root1", "name": "name1", + "version": "0.1", "mandatory": False, "order": 1, "ucum": { @@ -879,8 +1528,10 @@ def test_inactivate_odm_item(api_client): assert res["codelist"] == { "uid": "editable_cr", "name": "codelist attributes value1", + "version": "1.0", "submission_value": "codelist submission value1", "preferred_term": "codelist preferred term", + "allows_multi_choice": True, } assert res["terms"] == [ { @@ -894,7 +1545,7 @@ def test_inactivate_odm_item(api_client): } ] assert res["vendor_elements"] == [ - {"uid": "odm_vendor_element1", "name": "nameOne", "value": "value1"} + {"uid": "odm_vendor_element1", "name": "NameOne", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [ @@ -913,10 +1564,81 @@ def test_inactivate_odm_item(api_client): def test_cannot_add_odm_vendor_element_to_an_odm_item_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_element1", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-elements", json=data - ) + data = {} + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [{"uid": "odm_vendor_element1", "value": "value"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -929,10 +1651,80 @@ def test_cannot_add_odm_vendor_element_to_an_odm_item_that_is_in_retired_status( def test_cannot_add_odm_vendor_attribute_to_an_odm_item_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [{"uid": "odm_vendor_attribute1", "value": "value"}], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -945,10 +1737,82 @@ def test_cannot_add_odm_vendor_attribute_to_an_odm_item_that_is_in_retired_statu def test_cannot_add_odm_vendor_element_attribute_to_an_odm_item_that_is_in_retired_status( api_client, ): - data = [{"uid": "odm_vendor_attribute1", "value": "value"}] - response = api_client.post( - "concepts/odms/items/OdmItem_000001/vendor-element-attributes", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "prompt": "prompt1", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "sas_field_name1", + "sds_var_name": "sds_var_name1", + "origin": "origin1", + "comment": "comment1", + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", + }, + ], + "aliases": [{"context": "context1", "name": "name1"}], + "unit_definitions": [ + {"uid": "unit_definition_root1", "mandatory": False, "order": 1} + ], + "codelist": {"uid": "editable_cr", "allows_multi_choice": True}, + "terms": [ + { + "uid": "term_root_final", + "mandatory": True, + "order": 1, + "display_text": None, + "version": "1.0", + } + ], + "vendor_elements": [], + "vendor_element_attributes": [ + {"uid": "odm_vendor_attribute1", "value": "value"} + ], + "vendor_attributes": [], + "change_description": "desc doesnt change", + } + response = api_client.patch("concepts/odms/items/OdmItem_000001", json=data) assert_response_status_code(response, 400) @@ -1038,3 +1902,52 @@ def test_cannot_provide_only_one_of_length_or_significant_digits_when_datatype_i "error_code": "value_error", }, ] + + +@pytest.mark.parametrize( + "text_type", + [ + pytest.param("Description"), + pytest.param("Question"), + pytest.param("osb:DesignNotes"), + pytest.param("osb:CompletionInstructions"), + pytest.param("osb:DisplayText"), + ], +) +def test_cannot_add_duplicate_translated_texts(api_client, text_type: str): + data = { + "library_name": "Sponsor", + "name": "testing", + "oid": "testing", + "prompt": "testing", + "datatype": "string", + "length": 11, + "significant_digits": 11, + "sas_field_name": "testing", + "sds_var_name": "testing", + "origin": "testing", + "comment": "testing", + "translated_texts": [ + { + "text_type": text_type, + "language": "eng", + "text": str(r), + } + for r in range(2) + ], + "aliases": [], + "unit_definitions": [], + "codelist_uid": None, + "terms": [], + } + response = api_client.post("concepts/odms/items", json=data) + + assert_response_status_code(response, 400) + + res = response.json() + + assert res["type"] == "ValidationException" + assert ( + res["message"] + == f"Duplicate Translated Text found for text_type '{text_type}' and language 'eng'." + ) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods.py index dcbf94ec..253140fa 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods.py @@ -45,20 +45,46 @@ def test_creating_a_new_odm_method(api_client): "oid": "oid1", "method_type": "type1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -82,20 +108,46 @@ def test_creating_a_new_odm_method(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -122,20 +174,46 @@ def test_getting_non_empty_list_of_odm_methods(api_client): assert res["items"][0]["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["items"][0]["descriptions"] == [ + assert res["items"][0]["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["items"][0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -172,20 +250,46 @@ def test_getting_a_specific_odm_method(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -212,20 +316,46 @@ def test_getting_versions_of_a_specific_odm_method(api_client): assert res[0]["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res[0]["descriptions"] == [ + assert res[0]["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res[0]["aliases"] == [{"context": "context1", "name": "name1"}] @@ -240,20 +370,46 @@ def test_updating_an_existing_odm_method(api_client): "method_type": "type1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], "change_description": "name changed", - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -277,20 +433,46 @@ def test_updating_an_existing_odm_method(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -317,20 +499,46 @@ def test_getting_a_specific_odm_method_in_specific_version(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -357,20 +565,46 @@ def test_approving_an_odm_method(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -397,20 +631,46 @@ def test_inactivating_a_specific_odm_method(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", }, { - "name": "name3", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -437,20 +697,46 @@ def test_reactivating_a_specific_odm_method(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -477,20 +763,46 @@ def test_creating_a_new_odm_method_version(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", + "language": "eng", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -504,13 +816,11 @@ def test_create_a_new_odm_method_for_deleting_it(api_client): "oid": "oid2", "method_type": "type2", "formal_expressions": [], - "descriptions": [ + "translated_texts": [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ], "aliases": [], @@ -532,13 +842,11 @@ def test_create_a_new_odm_method_for_deleting_it(api_client): assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" assert res["formal_expressions"] == [] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ { - "name": "name - delete", + "text_type": "Description", "language": "eng", - "description": "description - delete", - "instruction": "instruction - delete", - "sponsor_instruction": "sponsor_instruction - delete", + "text": "name - delete", } ] assert res["aliases"] == [] @@ -558,14 +866,15 @@ def test_creating_a_new_odm_method_with_relations(api_client): "oid": "string", "method_type": "string", "formal_expressions": [{"context": "string", "expression": "string"}], - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string2"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, ], "aliases": [], } @@ -586,14 +895,15 @@ def test_creating_a_new_odm_method_with_relations(api_client): assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" assert res["formal_expressions"] == [{"context": "string", "expression": "string"}] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string2"}, { - "name": "string2", + "text_type": "osb:CompletionInstructions", "language": "eng", - "description": "string2", - "instruction": "string2", - "sponsor_instruction": "string2", + "text": "string2", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string2"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string2"}, ] assert res["aliases"] == [] assert res["possible_actions"] == ["approve", "delete", "edit"] @@ -610,14 +920,15 @@ def test_updating_an_existing_odm_method_with_relations(api_client): {"context": "context1", "expression": "expression1"}, {"context": "context4", "expression": "expression4"}, ], - "descriptions": [ + "translated_texts": [ + {"text_type": "Description", "language": "eng", "text": "string3"}, { - "name": "string3", - "language": "eng", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string3"}, ], "aliases": [{"context": "context1", "name": "name1"}], } @@ -641,14 +952,15 @@ def test_updating_an_existing_odm_method_with_relations(api_client): {"context": "context1", "expression": "expression1"}, {"context": "context4", "expression": "expression4"}, ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + {"text_type": "Description", "language": "eng", "text": "string3"}, { - "name": "string3", - "language": "eng", - "description": "string3", - "instruction": "string3", - "sponsor_instruction": "string3", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "string3", }, + {"text_type": "osb:DesignNotes", "language": "eng", "text": "string3"}, + {"text_type": "osb:DisplayText", "language": "eng", "text": "string3"}, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] assert res["possible_actions"] == ["approve", "edit"] diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods_negative.py index cc80d3df..643de666 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_methods_negative.py @@ -35,20 +35,46 @@ def test_create_a_new_odm_method(api_client): "oid": "oid1", "method_type": "type1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], - "descriptions": [ + "translated_texts": [ { - "name": "name2", + "text_type": "Description", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "description2", }, { - "name": "name3", + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", + "language": "eng", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -72,20 +98,46 @@ def test_create_a_new_odm_method(api_client): assert res["formal_expressions"] == [ {"context": "context1", "expression": "expression1"} ] - assert res["descriptions"] == [ + assert res["translated_texts"] == [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, { - "name": "name2", + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", }, { - "name": "name3", + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", + }, + { + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ] assert res["aliases"] == [{"context": "context1", "name": "name1"}] @@ -99,20 +151,46 @@ def test_cannot_create_a_new_odm_method_with_same_properties(api_client): "oid": "oid1", "method_type": "type1", "formal_expressions": [{"context": "context1", "expression": "expression1"}], - "descriptions": [ + "translated_texts": [ + { + "text_type": "Description", + "language": "eng", + "text": "description2", + }, + { + "text_type": "Description", + "language": "dan", + "text": "description3", + }, + { + "text_type": "osb:CompletionInstructions", + "language": "eng", + "text": "instruction2", + }, { - "name": "name2", + "text_type": "osb:CompletionInstructions", + "language": "dan", + "text": "instruction3", + }, + { + "text_type": "osb:DesignNotes", "language": "eng", - "description": "description2", - "instruction": "instruction2", - "sponsor_instruction": "sponsor_instruction2", + "text": "sponsor_instruction2", + }, + { + "text_type": "osb:DesignNotes", + "language": "dan", + "text": "sponsor_instruction3", }, { - "name": "name3", + "text_type": "osb:DisplayText", "language": "eng", - "description": "description3", - "instruction": "instruction3", - "sponsor_instruction": "sponsor_instruction3", + "text": "name2", + }, + { + "text_type": "osb:DisplayText", + "language": "dan", + "text": "name3", }, ], "aliases": [{"context": "context1", "name": "name1"}], @@ -137,13 +215,11 @@ def test_cannot_create_a_new_odm_method_without_an_english_description(api_clien "oid": "oid2", "type": "type2", "formal_expressions": [], - "descriptions": [ + "translated_texts": [ { - "name": "name - non-eng", + "text_type": "Description", "language": "DAN", - "description": "description - non-eng", - "instruction": "instruction - non-eng", - "sponsor_instruction": "sponsor_instruction - non-eng", + "text": "text - non-eng", } ], "aliases": [], @@ -156,7 +232,8 @@ def test_cannot_create_a_new_odm_method_without_an_english_description(api_clien assert res["type"] == "ValidationException" assert ( - res["message"] == "At least one description must be in English ('eng' or 'en')." + res["message"] + == "A Translated Text with text_type Description and language English ('eng' or 'en') must be provided." ) @@ -194,3 +271,42 @@ def test_cannot_reactivate_an_odm_method_that_is_not_retired(api_client): assert res["type"] == "BusinessLogicException" assert res["message"] == "Only RETIRED version can be reactivated." + + +@pytest.mark.parametrize( + "text_type", + [ + pytest.param("Description"), + pytest.param("Question"), + pytest.param("osb:DesignNotes"), + pytest.param("osb:CompletionInstructions"), + pytest.param("osb:DisplayText"), + ], +) +def test_cannot_add_duplicate_translated_texts(api_client, text_type: str): + data = { + "library_name": "Sponsor", + "name": "testing", + "oid": "testing", + "formal_expressions": [{"context": "context1", "expression": "expression1"}], + "translated_texts": [ + { + "text_type": text_type, + "language": "eng", + "text": str(r), + } + for r in range(2) + ], + "aliases": [], + } + response = api_client.post("concepts/odms/methods", json=data) + + assert_response_status_code(response, 400) + + res = response.json() + + assert res["type"] == "ValidationException" + assert ( + res["message"] + == f"Duplicate Translated Text found for text_type '{text_type}' and language 'eng'." + ) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_study_events.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_study_events.py index 6dbebfee..b4ced895 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_study_events.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_study_events.py @@ -250,6 +250,7 @@ def test_adding_odm_forms_to_a_specific_odm_study_event(api_client): assert res["forms"] == [ { "uid": "odm_form1", + "oid": "oid1", "name": "name1", "version": "1.0", "order_number": 1, @@ -295,6 +296,7 @@ def test_overriding_odm_forms_from_a_specific_odm_study_event(api_client): assert res["forms"] == [ { "uid": "odm_form2", + "oid": "oid2", "name": "name2", "version": "1.0", "order_number": 2, @@ -331,6 +333,7 @@ def test_approving_an_odm_study_event(api_client): assert res["forms"] == [ { "uid": "odm_form2", + "oid": "oid2", "name": "name2", "version": "1.0", "order_number": 2, @@ -367,6 +370,7 @@ def test_inactivating_a_specific_odm_study_event(api_client): assert res["forms"] == [ { "uid": "odm_form2", + "oid": "oid2", "name": "name2", "version": "1.0", "order_number": 2, @@ -403,6 +407,7 @@ def test_reactivating_a_specific_odm_study_event(api_client): assert res["forms"] == [ { "uid": "odm_form2", + "oid": "oid2", "name": "name2", "version": "1.0", "order_number": 2, @@ -439,6 +444,7 @@ def test_creating_a_new_odm_study_event_version(api_client): assert res["forms"] == [ { "uid": "odm_form2", + "oid": "oid2", "name": "name2", "version": "1.0", "order_number": 2, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes.py index 832f5761..7907dff5 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes.py @@ -193,12 +193,12 @@ def test_getting_versions_of_a_specific_odm_vendor_attribute(api_client): def test_updating_an_existing_odm_vendor_attribute(api_client): data = { "library_name": "Sponsor", - "name": "new name", + "name": "newName", "compatible_types": ["FormDef", "ItemRef"], "data_type": "string", "value_regex": "^[a-zA-Z]+$", "vendor_namespace_uid": "odm_vendor_namespace1", - "change_description": "regex changed and name changed to new name", + "change_description": "regex changed and name changed to newName", } response = api_client.patch( "concepts/odms/vendor-attributes/OdmVendorAttribute_000001", json=data @@ -210,14 +210,14 @@ def test_updating_an_existing_odm_vendor_attribute(api_client): assert res["uid"] == "OdmVendorAttribute_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "newName" assert res["compatible_types"] == ["FormDef", "ItemRef"] assert res["data_type"] == "string" assert res["value_regex"] == "^[a-zA-Z]+$" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.2" - assert res["change_description"] == "regex changed and name changed to new name" + assert res["change_description"] == "regex changed and name changed to newName" assert res["author_username"] == "unknown-user@example.com" assert res["vendor_namespace"] == { "uid": "odm_vendor_namespace1", @@ -276,7 +276,7 @@ def test_approving_an_odm_vendor_attribute(api_client): assert res["uid"] == "OdmVendorAttribute_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "newName" assert res["compatible_types"] == ["FormDef", "ItemRef"] assert res["data_type"] == "string" assert res["value_regex"] == "^[a-zA-Z]+$" @@ -309,7 +309,7 @@ def test_inactivating_a_specific_odm_vendor_attribute(api_client): assert res["uid"] == "OdmVendorAttribute_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "newName" assert res["compatible_types"] == ["FormDef", "ItemRef"] assert res["data_type"] == "string" assert res["value_regex"] == "^[a-zA-Z]+$" @@ -342,7 +342,7 @@ def test_reactivating_a_specific_odm_vendor_attribute(api_client): assert res["uid"] == "OdmVendorAttribute_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "newName" assert res["compatible_types"] == ["FormDef", "ItemRef"] assert res["data_type"] == "string" assert res["value_regex"] == "^[a-zA-Z]+$" @@ -375,7 +375,7 @@ def test_creating_a_new_odm_vendor_attribute_version(api_client): assert res["uid"] == "OdmVendorAttribute_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "newName" assert res["compatible_types"] == ["FormDef", "ItemRef"] assert res["data_type"] == "string" assert res["value_regex"] == "^[a-zA-Z]+$" @@ -489,7 +489,7 @@ def test_creating_a_new_odm_vendor_attribute_with_relation_to_odm_vendor_element assert res["vendor_namespace"] is None assert res["vendor_element"] == { "uid": "odm_vendor_element1", - "name": "nameOne", + "name": "NameOne", "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "status": "Final", "version": "1.0", diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes_negative.py index 1b5ee475..b89550ea 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_attributes_negative.py @@ -100,7 +100,7 @@ def test_create_a_new_odm_vendor_attribute_of_vendor_element(api_client): assert res["vendor_namespace"] is None assert res["vendor_element"] == { "uid": "odm_vendor_element1", - "name": "nameOne", + "name": "NameOne", "compatible_types": ["FormDef", "ItemGroupDef", "ItemDef"], "status": "Final", "version": "1.0", @@ -149,6 +149,39 @@ def test_cannot_create_a_new_odm_vendor_attribute_belonging_to_odm_vendor_namesp ) +def test_cannot_create_a_new_odm_vendor_attribute_without_first_char_lowercase( + api_client, +): + data = { + "library_name": "Sponsor", + "name": "Uppercase", + "compatible_types": ["FormDef"], + "data_type": "string", + "value_regex": None, + "vendor_namespace_uid": "odm_vendor_namespace1", + } + response = api_client.post("concepts/odms/vendor-attributes", json=data) + + assert_response_status_code(response, 400) + + res = response.json() + + assert res["type"] == "RequestValidationError" + assert res["details"] == [ + { + "ctx": { + "error": {}, + }, + "error_code": "value_error", + "field": [ + "body", + "name", + ], + "msg": "Value error, Provided value 'Uppercase' for 'name' is invalid. The first character must be lowercase.", + }, + ] + + def test_cannot_create_a_new_odm_vendor_attribute_belonging_to_odm_vendor_element_when_providing_compatible_types( api_client, ): diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements.py index e93b8861..9030b6f1 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements.py @@ -43,7 +43,7 @@ def test_getting_empty_list_of_odm_vendor_elements(api_client): def test_creating_a_new_odm_vendor_element(api_client): data = { "library_name": "Sponsor", - "name": "nameTwo", + "name": "NameTwo", "compatible_types": ["FormDef"], "vendor_namespace_uid": "odm_vendor_namespace1", } @@ -56,7 +56,7 @@ def test_creating_a_new_odm_vendor_element(api_client): assert res["compatible_types"] == ["FormDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "nameTwo" + assert res["name"] == "NameTwo" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.1" @@ -80,7 +80,7 @@ def test_creating_a_new_odm_vendor_element_with_relation_to_odm_vendor_element( ): data = { "library_name": "Sponsor", - "name": "nameThree", + "name": "NameThree", "compatible_types": ["FormDef"], "vendor_namespace_uid": "odm_vendor_namespace1", } @@ -93,7 +93,7 @@ def test_creating_a_new_odm_vendor_element_with_relation_to_odm_vendor_element( assert res["compatible_types"] == ["FormDef"] assert res["uid"] == "OdmVendorElement_000002" assert res["library_name"] == "Sponsor" - assert res["name"] == "nameThree" + assert res["name"] == "NameThree" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.1" @@ -122,7 +122,7 @@ def test_getting_non_empty_list_of_odm_vendor_elements(api_client): assert res["items"][0]["compatible_types"] == ["FormDef"] assert res["items"][0]["uid"] == "OdmVendorElement_000001" assert res["items"][0]["library_name"] == "Sponsor" - assert res["items"][0]["name"] == "nameTwo" + assert res["items"][0]["name"] == "NameTwo" assert res["items"][0]["end_date"] is None assert res["items"][0]["status"] == "Draft" assert res["items"][0]["version"] == "0.1" @@ -146,7 +146,7 @@ def test_getting_non_empty_list_of_odm_vendor_elements(api_client): assert res["items"][1]["author_username"] == "unknown-user@example.com" assert res["items"][1]["change_description"] == "Initial version" assert res["items"][1]["uid"] == "OdmVendorElement_000002" - assert res["items"][1]["name"] == "nameThree" + assert res["items"][1]["name"] == "NameThree" assert res["items"][1]["library_name"] == "Sponsor" assert res["items"][1]["vendor_namespace"] == { "uid": "odm_vendor_namespace1", @@ -168,7 +168,7 @@ def test_getting_possible_header_values_of_odm_vendor_elements(api_client): res = response.json() - assert res == ["nameThree", "nameTwo"] + assert res == ["NameThree", "NameTwo"] def test_getting_a_specific_odm_vendor_element(api_client): @@ -181,7 +181,7 @@ def test_getting_a_specific_odm_vendor_element(api_client): assert res["compatible_types"] == ["FormDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "nameTwo" + assert res["name"] == "NameTwo" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.1" @@ -212,7 +212,7 @@ def test_getting_versions_of_a_specific_odm_vendor_element(api_client): assert res[0]["compatible_types"] == ["FormDef"] assert res[0]["uid"] == "OdmVendorElement_000001" assert res[0]["library_name"] == "Sponsor" - assert res[0]["name"] == "nameTwo" + assert res[0]["name"] == "NameTwo" assert res[0]["end_date"] is None assert res[0]["status"] == "Draft" assert res[0]["version"] == "0.1" @@ -234,10 +234,10 @@ def test_getting_versions_of_a_specific_odm_vendor_element(api_client): def test_updating_an_existing_odm_vendor_element(api_client): data = { "library_name": "Sponsor", - "name": "new name", + "name": "NewName", "compatible_types": ["ItemDef"], "vendor_namespace_uid": "odm_vendor_namespace1", - "change_description": "name changed to new name", + "change_description": "name changed to NewName", } response = api_client.patch( "concepts/odms/vendor-elements/OdmVendorElement_000001", json=data @@ -250,11 +250,11 @@ def test_updating_an_existing_odm_vendor_element(api_client): assert res["compatible_types"] == ["ItemDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "NewName" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.2" - assert res["change_description"] == "name changed to new name" + assert res["change_description"] == "name changed to NewName" assert res["author_username"] == "unknown-user@example.com" assert res["vendor_namespace"] == { "uid": "odm_vendor_namespace1", @@ -281,7 +281,7 @@ def test_getting_a_specific_odm_vendor_element_in_specific_version(api_client): assert res["compatible_types"] == ["FormDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "nameTwo" + assert res["name"] == "NameTwo" assert res["end_date"] assert res["status"] == "Draft" assert res["version"] == "0.1" @@ -312,7 +312,7 @@ def test_approving_an_odm_vendor_element(api_client): assert res["compatible_types"] == ["ItemDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "NewName" assert res["end_date"] is None assert res["status"] == "Final" assert res["version"] == "1.0" @@ -343,7 +343,7 @@ def test_inactivating_a_specific_odm_vendor_element(api_client): assert res["compatible_types"] == ["ItemDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "NewName" assert res["end_date"] is None assert res["status"] == "Retired" assert res["version"] == "1.0" @@ -374,7 +374,7 @@ def test_reactivating_a_specific_odm_vendor_element(api_client): assert res["compatible_types"] == ["ItemDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "NewName" assert res["end_date"] is None assert res["status"] == "Final" assert res["version"] == "1.0" @@ -405,7 +405,7 @@ def test_creating_a_new_odm_vendor_element_version(api_client): assert res["compatible_types"] == ["ItemDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "new name" + assert res["name"] == "NewName" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "1.1" @@ -427,7 +427,7 @@ def test_creating_a_new_odm_vendor_element_version(api_client): def test_create_a_new_odm_vendor_element_for_deleting_it(api_client): data = { "library_name": "Sponsor", - "name": "nameDelete", + "name": "NameDelete", "compatible_types": ["FormDef"], "vendor_namespace_uid": "odm_vendor_namespace1", } @@ -439,7 +439,7 @@ def test_create_a_new_odm_vendor_element_for_deleting_it(api_client): assert res["compatible_types"] == ["FormDef"] assert res["uid"] == "OdmVendorElement_000003" - assert res["name"] == "nameDelete" + assert res["name"] == "NameDelete" assert res["library_name"] == "Sponsor" assert res["end_date"] is None assert res["status"] == "Draft" @@ -498,7 +498,7 @@ def test_create_a_new_odm_vendor_attribute_with_relation_to_odm_vendor_element( assert res["vendor_namespace"] is None assert res["vendor_element"] == { "uid": "OdmVendorElement_000001", - "name": "new name", + "name": "NewName", "compatible_types": ["ItemDef"], "status": "Draft", "version": "1.1", diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements_negative.py index 5b20b30f..ce9cc717 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_elements_negative.py @@ -33,7 +33,7 @@ def test_data(): def test_create_a_new_odm_vendor_element(api_client): data = { "library_name": "Sponsor", - "name": "nameTwo", + "name": "NameTwo", "compatible_types": ["FormDef"], "vendor_namespace_uid": "odm_vendor_namespace1", } @@ -46,7 +46,7 @@ def test_create_a_new_odm_vendor_element(api_client): assert res["compatible_types"] == ["FormDef"] assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "nameTwo" + assert res["name"] == "NameTwo" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.1" @@ -84,7 +84,7 @@ def test_cannot_create_a_new_odm_vendor_element_without_providing_compatible_typ ): data = { "library_name": "Sponsor", - "name": "nameTwo", + "name": "NameTwo", "compatible_types": [], "vendor_namespace_uid": "odm_vendor_namespace1", } @@ -94,6 +94,7 @@ def test_cannot_create_a_new_odm_vendor_element_without_providing_compatible_typ res = response.json() + assert res["type"] == "RequestValidationError" assert res["details"] == [ { "error_code": "too_short", @@ -104,12 +105,43 @@ def test_cannot_create_a_new_odm_vendor_element_without_providing_compatible_typ ] +def test_cannot_create_a_new_odm_vendor_element_without_first_char_uppercase( + api_client, +): + data = { + "library_name": "Sponsor", + "name": "lowercase", + "compatible_types": ["FormDef"], + "vendor_namespace_uid": "odm_vendor_namespace1", + } + response = api_client.post("concepts/odms/vendor-elements", json=data) + + assert_response_status_code(response, 400) + + res = response.json() + + assert res["type"] == "RequestValidationError" + assert res["details"] == [ + { + "ctx": { + "error": {}, + }, + "error_code": "value_error", + "field": [ + "body", + "name", + ], + "msg": "Value error, Provided value 'lowercase' for 'name' is invalid. The first character must be uppercase.", + }, + ] + + def test_cannot_create_a_new_odm_vendor_element_if_odm_vendor_namespace_doesnt_exist( api_client, ): data = { "library_name": "Sponsor", - "name": "nameTwo", + "name": "NameTwo", "compatible_types": ["FormDef"], "vendor_namespace_uid": "wrong_uid", } @@ -129,7 +161,7 @@ def test_cannot_create_a_new_odm_vendor_element_if_odm_vendor_namespace_doesnt_e def test_cannot_create_a_new_odm_vendor_element_with_existing_name(api_client): data = { "library_name": "Sponsor", - "name": "nameTwo", + "name": "NameTwo", "compatible_types": ["FormDef"], "vendor_namespace_uid": "odm_vendor_namespace1", } @@ -140,7 +172,7 @@ def test_cannot_create_a_new_odm_vendor_element_with_existing_name(api_client): res = response.json() assert res["type"] == "AlreadyExistsException" - assert res["message"] == "ODM Vendor Element with Name 'nameTwo' already exists." + assert res["message"] == "ODM Vendor Element with Name 'NameTwo' already exists." def test_cannot_inactivate_an_odm_vendor_element_that_is_in_draft_status(api_client): @@ -176,7 +208,7 @@ def test_create_an_odm_form(api_client): "oid": "oid1", "sdtm_version": "0.1", "repeating": "No", - "descriptions": [], + "translated_texts": [], "aliases": [], } response = api_client.post("concepts/odms/forms", json=data) @@ -196,7 +228,7 @@ def test_create_an_odm_form(api_client): assert res["version"] == "0.1" assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" - assert res["descriptions"] == [] + assert res["translated_texts"] == [] assert res["aliases"] == [] assert res["item_groups"] == [] assert res["vendor_elements"] == [] @@ -206,12 +238,22 @@ def test_create_an_odm_form(api_client): def test_add_odm_vendor_element_to_the_odm_form(api_client): - data = [{"uid": "OdmVendorElement_000001", "value": "value1"}] - response = api_client.post( - "concepts/odms/forms/OdmForm_000001/vendor-elements", json=data - ) + data = { + "library_name": "Sponsor", + "name": "name1", + "oid": "oid1", + "sdtm_version": "0.1", + "repeating": "No", + "translated_texts": [], + "aliases": [], + "vendor_elements": [{"uid": "OdmVendorElement_000001", "value": "value1"}], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "change desc", + } + response = api_client.patch("concepts/odms/forms/OdmForm_000001", json=data) - assert_response_status_code(response, 201) + assert_response_status_code(response, 200) res = response.json() @@ -226,11 +268,11 @@ def test_add_odm_vendor_element_to_the_odm_form(api_client): assert res["change_description"] == "Initial version" assert res["author_username"] == "unknown-user@example.com" assert res["sdtm_version"] == "0.1" - assert res["descriptions"] == [] + assert res["translated_texts"] == [] assert res["aliases"] == [] assert res["item_groups"] == [] assert res["vendor_elements"] == [ - {"uid": "OdmVendorElement_000001", "name": "nameTwo", "value": "value1"} + {"uid": "OdmVendorElement_000001", "name": "NameTwo", "value": "value1"} ] assert res["vendor_attributes"] == [] assert res["vendor_element_attributes"] == [] diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces.py index 665f886f..d3bf0fa7 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces.py @@ -376,7 +376,7 @@ def test_create_a_new_odm_vendor_attribute_with_relation_to_odm_vendor_namespace def test_create_a_new_odm_vendor_element1(api_client): data = { "library_name": "Sponsor", - "name": "nameTwo", + "name": "NameTwo", "compatible_types": ["FormDef"], "vendor_namespace_uid": "OdmVendorNamespace_000001", } @@ -388,7 +388,7 @@ def test_create_a_new_odm_vendor_element1(api_client): assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "nameTwo" + assert res["name"] == "NameTwo" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.1" diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces_negative.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces_negative.py index 0499222b..34b39cc5 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces_negative.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_vendor_namespaces_negative.py @@ -124,7 +124,7 @@ def test_create_odm_vendor_element_with_relation_to_the_odm_vendor_namespace( ): data = { "library_name": "Sponsor", - "name": "newName", + "name": "NewName", "compatible_types": ["FormDef"], "vendor_namespace_uid": "OdmVendorNamespace_000001", } @@ -136,7 +136,7 @@ def test_create_odm_vendor_element_with_relation_to_the_odm_vendor_namespace( assert res["uid"] == "OdmVendorElement_000001" assert res["library_name"] == "Sponsor" - assert res["name"] == "newName" + assert res["name"] == "NewName" assert res["end_date"] is None assert res["status"] == "Draft" assert res["version"] == "0.1" diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_exporter.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_exporter.py index 9bbb8412..aaa1c659 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_exporter.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_exporter.py @@ -33,6 +33,7 @@ STARTUP_ODM_XML_EXPORTER, STARTUP_UNIT_DEFINITIONS, ) +from clinical_mdr_api.tests.integration.utils.utils import TestUtils from clinical_mdr_api.tests.utils.checks import ( assert_response_content_type, assert_response_status_code, @@ -68,8 +69,154 @@ def test_data(): def test_get_odm_xml_form(api_client): + activity_instance_class = TestUtils.create_activity_instance_class( + name="Activity instance class 1", + definition="def Activity instance class 1", + is_domain_specific=True, + level=1, + ) + data_type_codelist = TestUtils.create_ct_codelist( + name="DATATYPE", submission_value="DATATYPE", extensible=True, approve=True + ) + data_type_term = TestUtils.create_ct_term( + sponsor_preferred_name="Data type", codelist_uid=data_type_codelist.codelist_uid + ) + role_codelist = TestUtils.create_ct_codelist( + name="ROLE", submission_value="ROLE", extensible=True, approve=True + ) + role_term = TestUtils.create_ct_term( + sponsor_preferred_name="Role", codelist_uid=role_codelist.codelist_uid + ) + activity_item_class = TestUtils.create_activity_item_class( + name="Activity Item Class name1", + order=1, + activity_instance_classes=[ + { + "uid": activity_instance_class.uid, + "mandatory": True, + "is_adam_param_specific_enabled": True, + "is_additional_optional": True, + "is_default_linked": True, + } + ], + role_uid=role_term.term_uid, + data_type_uid=data_type_term.term_uid, + ) + sub_group = TestUtils.create_activity_subgroup(name="activity_subgroup") + group = TestUtils.create_activity_group(name="activity_group") + + activity_instance = TestUtils.create_activity_instance( + name="name A", + activity_instance_class_uid=activity_instance_class.uid, + definition="def A", + abbreviation="abbr A", + nci_concept_id="NCIID", + nci_concept_name="NCINAME", + name_sentence_case="name A", + topic_code="topic code A", + is_research_lab=True, + adam_param_code="adam_code_a", + is_required_for_activity=True, + activities=[ + TestUtils.create_activity( + name="Activity", + activity_subgroups=[sub_group.uid], + activity_groups=[group.uid], + ).uid + ], + activity_subgroups=[sub_group.uid], + activity_groups=[group.uid], + activity_items=[ + { + "activity_item_class_uid": activity_item_class.uid, + "ct_terms": [], + "unit_definition_uids": [], + "is_adam_param_specific": True, + } + ], + ) + + api_client.post("concepts/odms/forms/odm_form1/versions") + api_client.post("concepts/odms/item-groups/odm_item_group1/versions") + + response = api_client.post( + "concepts/odms/items", + json={ + "name": "with activity instance", + "oid": "oid999", + "datatype": "string", + "prompt": None, + "length": 1, + "significant_digits": 1, + "sas_field_name": "sasfieldname999", + "sds_var_name": "sdsvarname999", + "origin": "origin999", + "comment": "comment999", + "descriptions": [], + "aliases": [], + "unit_definitions": [], + "codelist": None, + "terms": [], + }, + ) + assert_response_status_code(response, 201) + rs = response.json() + item_uid = rs["uid"] + + response = api_client.post( + "concepts/odms/item-groups/odm_item_group1/items", + json=[ + { + "uid": item_uid, + "order_number": 1, + "mandatory": "yes", + "vendor": {"attributes": []}, + } + ], + ) + assert_response_status_code(response, 201) + + response = api_client.patch( + "concepts/odms/items/" + item_uid, + json={ + "name": "with activity instance", + "oid": "oid999", + "datatype": "string", + "prompt": None, + "length": 1, + "significant_digits": 1, + "sas_field_name": "sasfieldname999", + "sds_var_name": "sdsvarname999", + "origin": "origin999", + "comment": "comment999", + "descriptions": [], + "aliases": [], + "unit_definitions": [], + "codelist": None, + "terms": [], + "activity_instances": [ + { + "activity_instance_uid": activity_instance.uid, + "activity_item_class_uid": activity_item_class.uid, + "odm_form_uid": "odm_form1", + "odm_item_group_uid": "odm_item_group1", + "order": 1, + "primary": True, + "preset_response_value": "preset_response_value1", + "value_condition": "value_condition1", + "value_dependent_map": "value_dependent_map1", + } + ], + "vendor_elements": [], + "vendor_element_attributes": [], + "vendor_attributes": [], + "change_description": "Added activity instance", + }, + ) + assert_response_status_code(response, 200) + response = api_client.post( - "concepts/odms/metadata/xmls/export?targets=odm_form1&target_type=form&stylesheet=blank&allowed_namespaces=*", + "concepts/odms/metadata/xmls/export?targets=odm_form1&target_type=form&stylesheet=falcon&allowed_namespaces=*", ) assert_response_status_code(response, 200) assert_response_content_type(response, CONTENT_TYPE) @@ -80,14 +227,15 @@ def test_get_odm_xml_form(api_client): expected_xml.set("FileOID", actual_xml.attrib["FileOID"]) expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) - assert '' in response.text + assert '' in response.text xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_forms(api_client): response = api_client.post( - "concepts/odms/metadata/xmls/export?targets=odm_form1&targets=odm_form2&target_type=form&stylesheet=blank&allowed_namespaces=*", + "concepts/odms/metadata/xmls/export?targets=odm_form1&targets=odm_form2&target_type=form&stylesheet=falcon&allowed_namespaces=*", ) assert_response_status_code(response, 200) assert_response_content_type(response, CONTENT_TYPE) @@ -98,14 +246,15 @@ def test_get_odm_xml_forms(api_client): expected_xml.set("FileOID", actual_xml.attrib["FileOID"]) expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) - assert '' in response.text + assert '' in response.text xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_forms_with_specific_version(api_client): response = api_client.post( - "concepts/odms/metadata/xmls/export?targets=odm_form1,1.0&targets=odm_form2,1.0&target_type=form&stylesheet=blank&allowed_namespaces=*", + "concepts/odms/metadata/xmls/export?targets=odm_form1,1.1&targets=odm_form2,1.0&target_type=form&stylesheet=falcon&allowed_namespaces=*", ) assert_response_status_code(response, 200) assert_response_content_type(response, CONTENT_TYPE) @@ -116,14 +265,15 @@ def test_get_odm_xml_forms_with_specific_version(api_client): expected_xml.set("FileOID", actual_xml.attrib["FileOID"]) expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) - assert '' in response.text + assert '' in response.text xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_forms_without_specific_version(api_client): response = api_client.post( - "concepts/odms/metadata/xmls/export?targets=odm_form1,&targets=odm_form2,&target_type=form&stylesheet=blank&allowed_namespaces=*", + "concepts/odms/metadata/xmls/export?targets=odm_form1,&targets=odm_form2,&target_type=form&stylesheet=falcon&allowed_namespaces=*", ) assert_response_status_code(response, 200) assert_response_content_type(response, CONTENT_TYPE) @@ -134,14 +284,15 @@ def test_get_odm_xml_forms_without_specific_version(api_client): expected_xml.set("FileOID", actual_xml.attrib["FileOID"]) expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) - assert '' in response.text + assert '' in response.text xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_item_group(api_client): response = api_client.post( - "concepts/odms/metadata/xmls/export?targets=odm_item_group1&target_type=item_group&stylesheet=blank&allowed_namespaces=*", + "concepts/odms/metadata/xmls/export?targets=odm_item_group1&target_type=item_group&stylesheet=falcon&allowed_namespaces=*", ) assert_response_status_code(response, 200) assert_response_content_type(response, CONTENT_TYPE) @@ -152,14 +303,15 @@ def test_get_odm_xml_item_group(api_client): expected_xml.set("FileOID", actual_xml.attrib["FileOID"]) expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) - assert '' in response.text + assert '' in response.text xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_item(api_client): response = api_client.post( - "concepts/odms/metadata/xmls/export?targets=odm_item1&target_type=item&stylesheet=blank&allowed_namespaces=*", + "concepts/odms/metadata/xmls/export?targets=odm_item1&target_type=item&stylesheet=falcon&allowed_namespaces=*", ) assert_response_status_code(response, 200) assert_response_content_type(response, CONTENT_TYPE) @@ -170,9 +322,10 @@ def test_get_odm_xml_item(api_client): expected_xml.set("FileOID", actual_xml.attrib["FileOID"]) expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) - assert '' in response.text + assert '' in response.text xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_with_allowed_namespaces(api_client): @@ -188,6 +341,7 @@ def test_get_odm_xml_with_allowed_namespaces(api_client): expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_with_mapper_csv(api_client): @@ -197,10 +351,6 @@ def test_get_odm_xml_with_mapper_csv(api_client): "mapper_file": ( "mapper.csv", "type,parent,from_name,to_name,to_alias,from_alias,alias_context\n" - "attribute,,osb:instruction,CompletionInstructions,,,\n" - "attribute,*,osb:sponsorInstruction,ImplementationNotes,,,\n" - "attribute,,CompletionInstructions,,true,,\n" - "attribute,*,ImplementationNotes,,true,,\n" "attribute,FormDef,osb:version,ov,,,\n" "element,,ItemRef,osb:ItemRef,,,\n" "element,FormDef,ItemGroupRef,osb:ItemGroupRef,,,\n" @@ -220,16 +370,25 @@ def test_get_odm_xml_with_mapper_csv(api_client): expected_xml.set("CreationDateTime", actual_xml.attrib["CreationDateTime"]) xml_diff(expected_xml, actual_xml) + xml_diff(actual_xml, expected_xml) def test_get_odm_xml_pdf_version(api_client): response = api_client.post( - "concepts/odms/metadata/xmls/export?target_type=form&targets=odm_form1&pdf=true&stylesheet=blank" + "concepts/odms/metadata/xmls/export?target_type=form&targets=odm_form1&pdf=true&stylesheet=falcon" ) assert_response_status_code(response, 200) assert_response_content_type(response, "application/pdf") +def test_get_odm_html_version(api_client): + response = api_client.post( + "concepts/odms/metadata/report?target_type=form&targets=odm_form1" + ) + assert_response_status_code(response, 200) + assert_response_content_type(response, "text/html") + + def test_throw_exception_if_target_type_is_not_supported(api_client): response = api_client.post( "concepts/odms/metadata/xmls/export?targets=study&target_type=study", diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_importer.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_importer.py index 6985bc8f..ecb6d855 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_importer.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_importer.py @@ -66,6 +66,7 @@ def test_import_odm_xml(api_client): res = response.json() assert_with_key_exclusion(IMPORT_OUTPUT1, res, ["start_date"]) + assert_with_key_exclusion(res, IMPORT_OUTPUT1, ["start_date"]) def test_import_odm_vendor_with_csv_mapper(api_client): @@ -77,11 +78,9 @@ def test_import_odm_vendor_with_csv_mapper(api_client): "mapper.csv", "type,parent,from_name,to_name,to_alias,from_alias,alias_context\n" "attribute,,Repeated,Repeating,,,\n" - "element,,NameOne,cs:nameOne,,,\n" + "element,,NameOne,cs:NameOne,,,\n" "element,,Alias,,,true,CompletionInstructions\n" "element,*,Alias,,,true,ImplementationNotes\n" - "attribute,,CompletionInstructions,osb:instruction,,,\n" - "attribute,*,ImplementationNotes,osb:sponsorInstruction,,,\n", "text/csv", ), }, @@ -92,6 +91,7 @@ def test_import_odm_vendor_with_csv_mapper(api_client): res = response.json() assert_with_key_exclusion(IMPORT_OUTPUT2, res, ["start_date"]) + assert_with_key_exclusion(res, IMPORT_OUTPUT2, ["start_date"]) def test_import_clinspark_odm_xml(api_client): @@ -106,6 +106,7 @@ def test_import_clinspark_odm_xml(api_client): res = response.json() assert_with_key_exclusion(CLINSPARK_OUTPUT, res, ["start_date"]) + assert_with_key_exclusion(res, CLINSPARK_OUTPUT, ["start_date"]) def test_throw_exception_if_file_is_not_xml(api_client): diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_stylesheets.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_stylesheets.py index a2ba133c..4b2ed562 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_stylesheets.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_odm_xml_stylesheets.py @@ -22,14 +22,14 @@ def test_get_available_stylesheet_names(api_client): response = api_client.get("concepts/odms/metadata/xmls/stylesheets") assert_response_status_code(response, 200) - assert response.json() == ["blank", "falcon", "with-annotations"] + assert response.json() == ["falcon", "with-annotations"] def test_get_specific_stylesheet(api_client): - response = api_client.get("concepts/odms/metadata/xmls/stylesheets/blank") + response = api_client.get("concepts/odms/metadata/xmls/stylesheets/falcon") with open( - settings.xml_stylesheet_dir_path + "blank.xsl", mode="r", encoding="utf-8" + settings.xml_stylesheet_dir_path + "falcon.xsl", mode="r", encoding="utf-8" ) as file: expected_xml = file.read() @@ -54,7 +54,7 @@ def test_throw_exception_if_stylesheet_doesnt_exist(api_client): def test_throw_exception_if_stylesheet_name_contains_disallowed_character(api_client): - for name in ["bla_nk", "blank.", "bla%nk"]: + for name in ["falc_on", "falcon.", "falc%on"]: response = api_client.get( f"concepts/odms/metadata/xmls/stylesheets/{name}", ) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_design_cells.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_design_cells.py index cf738006..48868529 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_design_cells.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_design_cells.py @@ -607,7 +607,7 @@ def test_adding_studydesigncell_selection_2nd(api_client): data = { "study_arm_uid": STUDY_ARM_2, "study_epoch_uid": "StudyEpoch_000001", - "study_element_uid": "StudyElement_000003", + "study_element_uid": "StudyElement_000002", "transition_rule": "Transition_Rule_2", } response = api_client.post("/studies/study_root/study-design-cells", json=data) @@ -629,7 +629,7 @@ def test_batch_patch_studydesigncell_selection(api_client): "content": { "study_design_cell_uid": "StudyDesignCell_000001", "study_arm_uid": "StudyArm_000002", - "study_element_uid": "StudyElement_000003", + "study_element_uid": "StudyElement_000002", "study_branch_arm_uid": None, }, }, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_elements.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_elements.py index b26dfaae..d620a24c 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_elements.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_elements.py @@ -418,7 +418,7 @@ def test_get_all_list_non_empty_for_multiple_elements(api_client): assert res["items"][0]["author_username"] == "unknown-user@example.com" assert res["items"][1]["study_uid"] == "study_root" assert res["items"][1]["study_version"] is not None - assert res["items"][1]["element_uid"] == "StudyElement_000003" + assert res["items"][1]["element_uid"] == "StudyElement_000002" assert res["items"][1]["order"] == 2 assert res["items"][1]["name"] == "Element_Name_2" assert res["items"][1]["short_name"] == "Element_Short_Name_2" @@ -558,7 +558,7 @@ def test_patch_specific_set_name(api_client): assert res["element_colour"] == "element_colour" assert res["author_username"] == "unknown-user@example.com" assert res["study_version"] is not None - assert res["element_uid"] == "StudyElement_000003" + assert res["element_uid"] == "StudyElement_000002" assert res["element_type"]["term_uid"] == "ElementType_0001" assert res["element_type"]["term_name"] == "Element Type" assert res["element_type"]["codelist_uid"] == "CTCodelist_ElementType" @@ -710,7 +710,7 @@ def test_reorder_specific1(api_client): assert res["element_colour"] == "element_colour" assert res["author_username"] == "unknown-user@example.com" assert res["study_version"] is not None - assert res["element_uid"] == "StudyElement_000003" + assert res["element_uid"] == "StudyElement_000002" assert res["element_type"]["term_uid"] == "ElementType_0001" assert res["element_type"]["term_name"] == "Element Type" assert res["element_type"]["codelist_uid"] == "CTCodelist_ElementType" @@ -842,7 +842,7 @@ def test_all_history_of_all_selection_study_elements( assert res[2]["project_number"] is None assert res[2]["project_name"] is None assert res[2]["study_version"] is None - assert res[2]["element_uid"] == "StudyElement_000003" + assert res[2]["element_uid"] == "StudyElement_000002" assert res[2]["name"] == "New_Element_Name_2" assert res[2]["short_name"] == "Element_Short_Name_2" assert res[2]["code"] == "Element_code_2" @@ -894,7 +894,7 @@ def test_all_history_of_all_selection_study_elements( assert res[3]["project_number"] is None assert res[3]["project_name"] is None assert res[3]["study_version"] is None - assert res[3]["element_uid"] == "StudyElement_000003" + assert res[3]["element_uid"] == "StudyElement_000002" assert res[3]["name"] == "New_Element_Name_2" assert res[3]["short_name"] == "Element_Short_Name_2" assert res[3]["code"] == "Element_code_2" @@ -949,7 +949,7 @@ def test_all_history_of_all_selection_study_elements( assert res[4]["project_number"] is None assert res[4]["project_name"] is None assert res[4]["study_version"] is None - assert res[4]["element_uid"] == "StudyElement_000003" + assert res[4]["element_uid"] == "StudyElement_000002" assert res[4]["name"] == "Element_Name_2" assert res[4]["short_name"] == "Element_Short_Name_2" assert res[4]["code"] == "Element_code_2" diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_endpoint.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_endpoint.py index d03a3583..58acae3c 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_endpoint.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/old/test_study_selection_endpoint.py @@ -729,7 +729,7 @@ def test_add_selection_2_no_timeframe_set(api_client): assert res["project_number"] == "123" assert res["project_name"] == "Project ABC" assert res["study_version"] is not None - assert res["study_endpoint_uid"] == "StudyEndpoint_000003" + assert res["study_endpoint_uid"] == "StudyEndpoint_000002" assert res["accepted_version"] is False assert res["study_objective"]["endpoint_count"] == 2 assert res["study_objective"]["accepted_version"] is False @@ -964,7 +964,7 @@ def test_check_list_has_2(api_client): # pylint:disable=too-many-statements assert res["items"][1]["project_number"] == "123" assert res["items"][1]["project_name"] == "Project ABC" assert res["items"][1]["study_version"] is not None - assert res["items"][1]["study_endpoint_uid"] == "StudyEndpoint_000003" + assert res["items"][1]["study_endpoint_uid"] == "StudyEndpoint_000002" assert res["items"][1]["accepted_version"] is False assert res["items"][1]["study_objective"]["endpoint_count"] == 2 assert res["items"][1]["study_objective"]["accepted_version"] is False @@ -1151,7 +1151,7 @@ def test_patch_specific_new_study_objective(api_client): assert res["project_number"] == "123" assert res["project_name"] == "Project ABC" assert res["study_version"] is not None - assert res["study_endpoint_uid"] == "StudyEndpoint_000003" + assert res["study_endpoint_uid"] == "StudyEndpoint_000002" assert res["accepted_version"] is False assert res["study_objective"]["accepted_version"] is False assert res["study_objective"]["endpoint_count"] == 2 @@ -1225,7 +1225,7 @@ def test_patch_specific_remove_study_objective(api_client): assert res["project_number"] == "123" assert res["project_name"] == "Project ABC" assert res["study_version"] is not None - assert res["study_endpoint_uid"] == "StudyEndpoint_000003" + assert res["study_endpoint_uid"] == "StudyEndpoint_000002" assert res["accepted_version"] is False assert res["study_objective"] is None assert res["study_uid"] == "study_root" @@ -1352,7 +1352,7 @@ def test_adding_selection_create(api_client): assert res["project_name"] == "Project ABC" assert res["study_uid"] == "study_root" assert res["study_version"] is not None - assert res["study_endpoint_uid"] == "StudyEndpoint_000008" + assert res["study_endpoint_uid"] == "StudyEndpoint_000003" assert res["study_objective"]["endpoint_count"] == 2 assert res["study_objective"]["accepted_version"] is False assert res["study_objective"]["start_date"] is not None diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_design_listings/test_study_metadata_listings.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_design_listings/test_study_metadata_listings.py index 9ec21012..6df67874 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_design_listings/test_study_metadata_listings.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_design_listings/test_study_metadata_listings.py @@ -553,7 +553,7 @@ def test_study_metadata_listing_api(api_client): "desc": "desc...", }, { - "uid": "StudyElement_000003", + "uid": "StudyElement_000002", "order": 2, "name": "Element_Name_1", "short_name": "Element_Short_Name_1", @@ -582,7 +582,7 @@ def test_study_metadata_listing_api(api_client): "arm_uid": "StudyArm_000001", "branch_uid": "", "epoch_uid": "StudyEpoch_000002", - "element_uid": "StudyElement_000003", + "element_uid": "StudyElement_000002", }, { "arm_uid": "StudyArm_000003", @@ -997,7 +997,7 @@ def test_study_metadata_listing_with_subpart(api_client): "desc": "desc...", }, { - "uid": "StudyElement_000003", + "uid": "StudyElement_000002", "order": 2, "name": "Element_Name_1", "short_name": "Element_Short_Name_1", @@ -1026,7 +1026,7 @@ def test_study_metadata_listing_with_subpart(api_client): "arm_uid": "StudyArm_000001", "branch_uid": "", "epoch_uid": "StudyEpoch_000002", - "element_uid": "StudyElement_000003", + "element_uid": "StudyElement_000002", }, { "arm_uid": "StudyArm_000003", diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_activity_instances.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_activity_instances.py index d91efa11..852d7594 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_activity_instances.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_activity_instances.py @@ -2779,3 +2779,78 @@ def test_batch_patch_study_activity_instance_data_supplier_and_origin_fields( assert res["origin_source"]["term_uid"] == origin_source_term.term_uid TestUtils.delete_study(test_study.uid) + + +@pytest.mark.parametrize( + "field,invalid_uid,expected_msg", + [ + ( + "origin_type_uid", + "InvalidUid_1", + "Origin Type Term with UID 'InvalidUid_1' doesn't exist.", + ), + ( + "origin_source_uid", + "InvalidUid_2", + "Origin Source Term with UID 'InvalidUid_2' doesn't exist.", + ), + ], +) +def test_study_activity_instance_invalid_origin_uid( + api_client, field, invalid_uid, expected_msg +): + """Test that invalid origin UIDs return 400 error.""" + test_study = TestUtils.create_study(project_number=project.project_number) + response = api_client.post( + f"/studies/{test_study.uid}/study-activities", + json={ + "activity_uid": randomized_activity.uid, + "activity_subgroup_uid": randomisation_activity_subgroup.uid, + "activity_group_uid": general_activity_group.uid, + "soa_group_term_uid": term_efficacy_uid, + }, + ) + assert_response_status_code(response, 201) + study_activity_uid = response.json()["study_activity_uid"] + + response = api_client.post( + f"/studies/{test_study.uid}/study-activity-instances", + json={ + "study_activity_uid": study_activity_uid, + "activity_instance_uid": second_randomized_activity_instance.uid, + field: invalid_uid, + }, + ) + assert_response_status_code(response, 400) + assert response.json()["message"] == expected_msg + TestUtils.delete_study(test_study.uid) + + +def test_study_activity_instance_origin_term_not_in_codelist(api_client): + """Test that using a term from wrong codelist returns 400 error.""" + test_study = TestUtils.create_study(project_number=project.project_number) + response = api_client.post( + f"/studies/{test_study.uid}/study-activities", + json={ + "activity_uid": randomized_activity.uid, + "activity_subgroup_uid": randomisation_activity_subgroup.uid, + "activity_group_uid": general_activity_group.uid, + "soa_group_term_uid": term_efficacy_uid, + }, + ) + assert_response_status_code(response, 201) + study_activity_uid = response.json()["study_activity_uid"] + + # Use supplier_type_term which exists but is NOT in ORIGINT codelist + response = api_client.post( + f"/studies/{test_study.uid}/study-activity-instances", + json={ + "study_activity_uid": study_activity_uid, + "activity_instance_uid": second_randomized_activity_instance.uid, + "origin_type_uid": supplier_type_term.term_uid, + }, + ) + assert_response_status_code(response, 400) + res = response.json() + assert "was not found in the codelist" in res["message"] + TestUtils.delete_study(test_study.uid) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_arms.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_arms.py index c3e2bbc8..32b8d6d1 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_arms.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_arms.py @@ -145,6 +145,7 @@ def test_arm_modify_actions_on_locked_study(api_client): json={ "name": "Arm_Name_1", "short_name": "Arm_Short_Name_1", + "label": "Arm_Label_1", "code": "Arm_code_1", "description": "desc...", "randomization_group": "Randomization_Group_1", @@ -182,6 +183,7 @@ def test_arm_modify_actions_on_locked_study(api_client): json={ "name": "Arm_Name_2", "short_name": "Arm_Short_Name_2", + "label": "Arm_Label_2", "code": "Arm_code_2", "description": "desc...", "randomization_group": "Randomization_Group_2", @@ -278,6 +280,7 @@ def test_study_arm_type_version_selecting_ct_package(api_client): json={ "name": "Arm_Name_1" + suffix_txt, "short_name": "Arm_Short_Name_1" + suffix_txt, + "label": "Arm_Label_1" + suffix_txt, "code": "Arm_code_1" + suffix_txt, "description": "desc..." + suffix_txt, "randomization_group": "Randomization_Group_1" + suffix_txt, @@ -379,6 +382,7 @@ def test_study_arm_ct_term_retrieval_at_date(api_client): json={ "name": "Arm_Name_1" + suffix_txt, "short_name": "Arm_Short_Name_1" + suffix_txt, + "label": "Arm_Label_1" + suffix_txt, "code": "Arm_code_1" + suffix_txt, "description": "desc..." + suffix_txt, "randomization_group": "Randomization_Group_1" + suffix_txt, @@ -428,7 +432,9 @@ def test_get_study_arms_csv_xml_excel(api_client, export_format): def test_batch_operations(api_client): test_study = TestUtils.create_study() arm_name_1 = "Arm_Name_1" + arm_label_1 = "Arm_Label_1" arm_name_2 = "Arm_Name_2" + arm_label_2 = "Arm_Label_2" response = api_client.post( f"/studies/{test_study.uid}/study-arms/batch", json=[ @@ -437,6 +443,7 @@ def test_batch_operations(api_client): "content": { "name": arm_name_1, "short_name": "Arm_Short_Name_1", + "label": arm_label_1, "code": "Arm_code_1", "description": "desc...", "randomization_group": "Randomization_Group_1", @@ -450,6 +457,7 @@ def test_batch_operations(api_client): "name": arm_name_2, "short_name": "Arm_Short_Name_2", "code": "Arm_code_2", + "label": arm_label_2, "description": "desc...", "randomization_group": "Randomization_Group_2", "number_of_subjects": 2, @@ -462,9 +470,11 @@ def test_batch_operations(api_client): res = response.json() assert res[0]["response_code"] == 201 assert res[0]["content"]["name"] == arm_name_1 + assert res[0]["content"]["label"] == arm_label_1 assert res[0]["content"]["merge_branch_for_this_arm_for_sdtm_adam"] is True assert res[1]["response_code"] == 201 assert res[1]["content"]["name"] == arm_name_2 + assert res[1]["content"]["label"] == arm_label_2 assert res[1]["content"]["merge_branch_for_this_arm_for_sdtm_adam"] is False study_arm_1_uid = res[0]["content"]["arm_uid"] study_arm_2_uid = res[1]["content"]["arm_uid"] @@ -578,6 +588,7 @@ def test_study_arm_is_not_updated_when_same_payload_is_sent(api_client): arm_type_uid=investigational_arm.term_uid, name="Arm 1 name", short_name="Arm 1 short name", + label="Arm 1 label", description="Arm 1 description", ) @@ -587,6 +598,7 @@ def test_study_arm_is_not_updated_when_same_payload_is_sent(api_client): json={ "name": "Arm 1 name", "short_name": "Arm 1 short name", + "label": "Arm 1 label", "description": "Arm 1 description", "arm_type_uid": investigational_arm.term_uid, }, @@ -595,6 +607,7 @@ def test_study_arm_is_not_updated_when_same_payload_is_sent(api_client): res = response.json() assert res["name"] == "Arm 1 name" assert res["short_name"] == "Arm 1 short name" + assert res["label"] == "Arm 1 label" assert res["description"] == "Arm 1 description" assert res["arm_type"]["term_uid"] == investigational_arm.term_uid diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_flowchart.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_flowchart.py index 3a0b4b8b..2da23d9e 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_flowchart.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_flowchart.py @@ -86,6 +86,7 @@ OPERATIONAL_SOA_EXPORT_COLUMN_HEADERS = [ "study_number", "study_version", + "library", "soa_group", "activity_group", "activity_subgroup", @@ -102,6 +103,7 @@ OPERATIONAL_SOA_EXPORT_COLUMN_HEADERS_XLSX = [ "Study number", "Study version", + "Library", "SoA group", "Activity group", "Activity subgroup", @@ -1089,7 +1091,7 @@ def test_operational_soa_csv( rows = list(csv.reader(response.iter_lines(), dialect=csv.excel)) # check dimensions - assert len(rows[0]) == 11, "Number of columns mismatch" + assert len(rows[0]) == 12, "Number of columns mismatch" assert ( len(rows) == soa_test_data.NUM_OPERATIONAL_SOA_EXPORT_ROWS + 1 ), "Number of rows mismatch" @@ -1102,13 +1104,15 @@ def test_operational_soa_xlsx( """Tests XLS export of Operational SoA""" num_header_rows = 4 - num_header_cols = 8 + num_header_cols = 10 expected_column_headers = [ "lowest visibility layer", + "Library", "SoA group", "Group", "Subgroup", "Activity", + "Instance", "Topic Code", "ADaM Param Code", "Visits", @@ -1139,16 +1143,16 @@ def test_operational_soa_xlsx( num_rows = len(rows) assert num_rows > num_header_rows, "worksheet 0 too few rows" assert ( - num_rows == num_header_rows + soa_test_data.NUM_ACTIVITY_INSTANCES - ), "number of rows mismatch study-activity-instances" + num_rows == num_header_rows + soa_test_data.NUM_OPERATIONAL_SOA_XLSX_DATA_ROWS + ), "number of rows mismatch (activities + instances)" # check headers assert "study_version: " in worksheet["A1"].value.strip() assert "study_number: " in worksheet["A2"].value.strip() assert "Date/time of extraction: " in worksheet["A3"].value.strip() assert "By: " in worksheet["D3"].value.strip() - assert "Epochs" in worksheet["H3"].value - column_headers = [cell.value for row in worksheet["A4:H4"] for cell in row] + assert "Epochs" in worksheet["J3"].value + column_headers = [cell.value for row in worksheet["A4:J4"] for cell in row] assert column_headers == expected_column_headers, "Column headers mismatch" # verify the number of checkmarks diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_visits.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_visits.py index 7c3bcc2d..4fc6d665 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_visits.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/study_selections/test_study_visits.py @@ -680,6 +680,36 @@ def test_non_manually_defined_visit(api_client): # failed post on the uniqueness check for non_manually defined visit + # Create a non_manually (ANCHOR_VISIT_IN_GROUP_OF_SUBV) defined study visit with existed visit number, unique visit number, visit name and visit short name + # When A study visit is created or updated + # And The study visit is not defined as a "Manually defined visit" + # And The is defined with a derived or preset test value that already exist for a manually defined study visit + inputs_visit = { + "study_epoch_uid": study_epoch.uid, + "visit_type_uid": "VisitType_0002", + "time_reference_uid": "VisitSubType_0005", + "time_value": 22, + "time_unit_uid": DAYUID, + "visit_class": "SINGLE_VISIT", + "visit_subclass": "ANCHOR_VISIT_IN_GROUP_OF_SUBV", + "is_global_anchor_visit": False, + } + + datadict = visits_basic_data.copy() + datadict.update(inputs_visit) + response = api_client.post( + f"/studies/{study_for_i_visit.uid}/study-visits", + json=datadict, + ) + + # Then The system displays the message "Value \"test value\" in field "" is not unique for the study + # as a manually defined value exist. Change the manually defined value before this visit can be defined." + assert_response_status_code(response, 400) + res = response.json() + error_msg = "Fields visit number - 2 and unique visit number - 200 and visit name - Visit 2 are not unique" + error_msg += " for the Study as a manually defined value exists. Change the manually defined value before this visit can be defined." + assert res["message"] == error_msg + # Create a non_manually defined study visit with existed visit number, unique visit number, visit name and visit short name # When A study visit is created or updated # And The study visit is not defined as a "Manually defined visit" @@ -2302,7 +2332,7 @@ def test_assert_uvn_is_changed_when_group_of_visits_is_modified(api_client): study_epoch = create_study_epoch("EpochSubType_0001", study_uid=study.uid) # Global Anchor Visit - inputs = { + anchor_inputs = { "study_epoch_uid": study_epoch.uid, "visit_type_uid": "VisitType_0002", "time_reference_uid": "VisitSubType_0005", @@ -2313,7 +2343,7 @@ def test_assert_uvn_is_changed_when_group_of_visits_is_modified(api_client): "is_global_anchor_visit": True, } datadict = visits_basic_data - datadict.update(inputs) + datadict.update(anchor_inputs) response = api_client.post( f"/studies/{study.uid}/study-visits", json=datadict, @@ -2383,6 +2413,92 @@ def test_assert_uvn_is_changed_when_group_of_visits_is_modified(api_client): assert audit_trail[1]["unique_visit_number"] == 110 assert audit_trail[2]["unique_visit_number"] == 100 + new_visit_description = "Edited Anchor Visit" + anchor_inputs.update( + {"description": new_visit_description, "uid": anchor_visit_uid} + ) + datadict = visits_basic_data + datadict.update(anchor_inputs) + response = api_client.patch( + f"/studies/{study.uid}/study-visits/{anchor_visit_uid}", + json=datadict, + ) + assert_response_status_code(response, 200) + assert response.json()["description"] == new_visit_description + + response = api_client.get( + f"/studies/{study.uid}/study-visits", + ) + assert_response_status_code(response, 200) + study_visits = response.json()["items"] + assert len(study_visits) == 2 + assert study_visits[0]["uid"] == anchor_visit_uid + assert study_visits[0]["visit_short_name"] == "V1D1" + assert study_visits[0]["visit_name"] == "Visit 1" + assert study_visits[0]["unique_visit_number"] == 100 + assert study_visits[1]["uid"] == subvisit_uid + assert study_visits[1]["visit_short_name"] == "V1D11" + assert study_visits[1]["visit_name"] == "Visit 1" + assert study_visits[1]["unique_visit_number"] == 110 + + # Call for StudyVisits but derive properties based on visit_number instead of returning them straight from DB + response = api_client.get( + f"/studies/{study.uid}/study-visits", + params={"derive_props_based_on_timeline": True}, + ) + assert_response_status_code(response, 200) + study_visits = response.json()["items"] + assert len(study_visits) == 2 + assert study_visits[0]["uid"] == anchor_visit_uid + assert study_visits[0]["visit_short_name"] == "V1D1" + assert study_visits[0]["visit_name"] == "Visit 1" + assert study_visits[0]["unique_visit_number"] == 100 + assert study_visits[1]["uid"] == subvisit_uid + assert study_visits[1]["visit_short_name"] == "V1D11" + assert study_visits[1]["visit_name"] == "Visit 1" + assert study_visits[1]["unique_visit_number"] == 110 + + response = api_client.get( + f"/studies/{study.uid}/study-visits/{anchor_visit_uid}", + ) + assert_response_status_code(response, 200) + study_visit = response.json() + assert study_visit["uid"] == anchor_visit_uid + assert study_visit["visit_short_name"] == "V1D1" + assert study_visit["visit_name"] == "Visit 1" + assert study_visit["unique_visit_number"] == 100 + response = api_client.get( + f"/studies/{study.uid}/study-visits/{subvisit_uid}", + ) + assert_response_status_code(response, 200) + study_visit = response.json() + assert study_visit["uid"] == subvisit_uid + assert study_visit["visit_short_name"] == "V1D11" + assert study_visit["visit_name"] == "Visit 1" + assert study_visit["unique_visit_number"] == 110 + + # Call for StudyVisits but derive properties based on visit_number instead of returning them straight from DB + response = api_client.get( + f"/studies/{study.uid}/study-visits/{anchor_visit_uid}", + params={"derive_props_based_on_timeline": True}, + ) + assert_response_status_code(response, 200) + study_visit = response.json() + assert study_visit["uid"] == anchor_visit_uid + assert study_visit["visit_short_name"] == "V1D1" + assert study_visit["visit_name"] == "Visit 1" + assert study_visit["unique_visit_number"] == 100 + response = api_client.get( + f"/studies/{study.uid}/study-visits/{subvisit_uid}", + params={"derive_props_based_on_timeline": True}, + ) + assert_response_status_code(response, 200) + study_visit = response.json() + assert study_visit["uid"] == subvisit_uid + assert study_visit["visit_short_name"] == "V1D11" + assert study_visit["visit_name"] == "Visit 1" + assert study_visit["unique_visit_number"] == 110 + def test_it_is_possible_create_two_study_visits_with_the_same_timing(api_client): test_study = TestUtils.create_study(project_number=project.project_number) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/test_iso.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/test_iso.py new file mode 100644 index 00000000..b3d60f71 --- /dev/null +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/test_iso.py @@ -0,0 +1,44 @@ +""" +Tests for /admin/* endpoints +""" + +import logging + +import pytest +from fastapi.testclient import TestClient + +from clinical_mdr_api.main import app +from clinical_mdr_api.tests.utils.checks import assert_response_status_code + +# pylint: disable=unused-argument +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments + +# pytest fixture functions have other fixture functions as arguments, +# which pylint interprets as unused arguments + + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def api_client(): + """Create FastAPI test client""" + yield TestClient(app) + + +def test_get_iso_languages(api_client): + response = api_client.get("/iso/639") + assert_response_status_code(response, 200) + + rs = response.json() + + assert len(rs) == 181, f"Expected 181 languages, but got {len(rs)}" + + for lang in rs: + assert ( + len(lang["_1"]) == 2 + ), f"Key '{lang["_1"]}' must be 2 characters long for 639-1" + assert ( + len(lang["_2T"]) == 3 + ), f"Key '{lang["_2T"]}' must be 3 characters long for 639-2T" diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/test_studies.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/test_studies.py index b624d108..72f7fd0c 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/api/test_studies.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/api/test_studies.py @@ -14,6 +14,7 @@ import json import logging import random +import threading from string import ascii_lowercase from typing import Sequence from unittest import mock @@ -3118,3 +3119,109 @@ def test_get_study_complexity_score(api_client): res_version_1 = response.json() assert isinstance(res_version_1, float) assert res_version_1 >= 0 + + +def test_concurrent_study_locking_does_not_create_duplicate_sponsor_ct_packages( + api_client, +): + """Test that locking multiple studies concurrently does not create duplicate Sponsor CT Packages""" + from datetime import date + + # Create multiple studies for concurrent locking + study1 = TestUtils.create_study() + study2 = TestUtils.create_study() + study3 = TestUtils.create_study() + + # Update study titles to be able to lock them + for study_obj in [study1, study2, study3]: + response = api_client.patch( + f"/studies/{study_obj.uid}", + json={ + "current_metadata": {"study_description": {"study_title": "test title"}} + }, + ) + assert_response_status_code(response, 200) + + # Get initial count of sponsor packages for today + response = api_client.get( + f"/ct/packages?sponsor_only=true&catalogue_name={settings.sdtm_ct_catalogue_name}" + ) + assert_response_status_code(response, 200) + initial_packages = response.json() + initial_count_today = len( + [ + pkg + for pkg in initial_packages + if pkg.get("effective_date") == date.today().isoformat() + ] + ) + + # Lock studies concurrently using threads + lock_results = [] + errors = [] + + def lock_study(study_uid): + try: + response = api_client.post( + f"/studies/{study_uid}/locks", + json={"change_description": f"Concurrent lock test for {study_uid}"}, + ) + lock_results.append((study_uid, response.status_code, response.json())) + except (ValueError, KeyError, TypeError) as e: + # Catch specific exceptions that might occur during API call processing + errors.append((study_uid, str(e))) + + threads = [ + threading.Thread(target=lock_study, args=(study1.uid,)), + threading.Thread(target=lock_study, args=(study2.uid,)), + threading.Thread(target=lock_study, args=(study3.uid,)), + ] + + # Start all threads + for thread in threads: + thread.start() + + # Wait for all threads to complete + for thread in threads: + thread.join() + + # Verify we got results from all threads + assert ( + len(lock_results) == 3 + ), f"Expected 3 lock attempts, got {len(lock_results)}. Errors: {errors}" + + # Count successful locks (some may fail due to other validation, but that's OK for this test) + successful_locks = [r for r in lock_results if r[1] == 201] + assert len(successful_locks) > 0, ( + f"No successful locks. Results: {lock_results}. " + f"This test focuses on preventing duplicate packages, not all locks succeeding." + ) + + # Verify only one additional sponsor package was created (or none if it already existed) + response = api_client.get( + f"/ct/packages?sponsor_only=true&catalogue_name={settings.sdtm_ct_catalogue_name}" + ) + assert_response_status_code(response, 200) + final_packages = response.json() + final_count_today = len( + [ + pkg + for pkg in final_packages + if pkg.get("effective_date") == date.today().isoformat() + ] + ) + + # Should have at most one more package than initially (if it didn't exist before) + # or the same count (if it already existed and was reused) + assert final_count_today <= initial_count_today + 1 + assert final_count_today >= initial_count_today + + # Verify no duplicate packages exist (same name and date) + package_names_today = [ + pkg["name"] + for pkg in final_packages + if pkg.get("effective_date") == date.today().isoformat() + ] + assert len(package_names_today) == len( + set(package_names_today) + ), f"Duplicate sponsor packages found: {package_names_today}" diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/domain_repositories/test_study_definition_repository.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/domain_repositories/test_study_definition_repository.py index f1a8fbe6..a6518425 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/domain_repositories/test_study_definition_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/domain_repositories/test_study_definition_repository.py @@ -1,7 +1,6 @@ # pylint: disable=unused-argument import dataclasses -import sys import unittest import pytest @@ -793,7 +792,7 @@ def test__find_all__results(self): with db.transaction: repo = StudyDefinitionRepositoryImpl(current_function_name()) all_studies_in_db = repo.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items repo.close() @@ -832,7 +831,7 @@ def test__find_all__with_custom_sort_order__success(self): with db.transaction: repo = StudyDefinitionRepositoryImpl(current_function_name()) all_studies_in_db = repo.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items repo.close() diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/repositories/concurrency/test_control_terminology.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/repositories/concurrency/test_control_terminology.py index 7f4a241f..41b59e37 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/repositories/concurrency/test_control_terminology.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/repositories/concurrency/test_control_terminology.py @@ -93,7 +93,7 @@ def set_up_base_graph_for_control_terminology(self): preferred_term="Preferred Term", definition="Definition", extensible=True, - ordinal=False, + is_ordinal=False, ) ct_codelist_attributes_ar = CTCodelistAttributesAR.from_input_values( diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_flowchart.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_flowchart.py index aa0c29c4..b3c5a3db 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_flowchart.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_flowchart.py @@ -726,7 +726,7 @@ def test_download_operational_soa_content( study_activity = study_activities_map[sched.study_activity_uid] study_visit = study_visits_map[sched.study_visit_uid] - assert len(res.keys()) == 12, f"record #{i} property count mismatch" + assert len(res.keys()) == 13, f"record #{i} property count mismatch" assert res["study_version"].startswith("LATEST on 20") assert ( res["study_number"] diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_visits.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_visits.py index d46b30e6..8bde10f8 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_visits.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/services/test_study_visits.py @@ -704,7 +704,7 @@ def test__create_subvisits_uvn__reordered_successfully(self): # we add so many subvists as there is a logic of # recalculating subvists unique-visit-numbers when we exceed allowed limits for i in range(1, 21): - create_visit_with_update( + vis = create_visit_with_update( study_epoch_uid=self.epoch2.uid, visit_type_uid="VisitType_0003", time_reference_uid="VisitSubType_0005", @@ -715,6 +715,7 @@ def test__create_subvisits_uvn__reordered_successfully(self): visit_class="SINGLE_VISIT", visit_subclass="ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV", ) + print(f"created subvisit {i} with uvn {vis.unique_visit_number}") # check unique visit numbers before recalculation if i == 9: all_visits = visit_service.get_all_visits( diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/data_library.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/data_library.py index faca1323..c51bebf9 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/data_library.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/data_library.py @@ -42,7 +42,10 @@ } AS final_properties MERGE (library:Library {name:"Sponsor", is_editable:true}) -MERGE (odm_description1:OdmDescription {name: "name1", language: "en", description: "description1", instruction: "instruction1", sponsor_instruction: "sponsor_instruction1"}) +MERGE (odm_translated_text1:OdmTranslatedText {text_type: "Description", language: "en", text: "Description1"}) +MERGE (odm_translated_text2:OdmTranslatedText {text_type: "osb:CompletionInstructions", language: "en", text: "Completion Instructions1"}) +MERGE (odm_translated_text3:OdmTranslatedText {text_type: "osb:DesignNotes", language: "en", text: "Design Notes1"}) +MERGE (odm_translated_text4:OdmTranslatedText {text_type: "osb:DisplayText", language: "en", text: "Display Text1"}) MERGE (odm_formal_expression1:OdmFormalExpression {context: "context1", expression: "expression1"}) MERGE (library)-[:CONTAINS_CONCEPT]->(odm_condition_root1:ConceptRoot:OdmConditionRoot {uid: "odm_condition1"}) @@ -59,8 +62,14 @@ MERGE (odm_condition_root2)-[hv2:HAS_VERSION]->(odm_condition_value2) SET hv2 = final_properties -MERGE (odm_condition_value1)-[:HAS_DESCRIPTION]->(odm_description1) -MERGE (odm_condition_value2)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (odm_condition_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (odm_condition_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (odm_condition_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (odm_condition_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (odm_condition_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (odm_condition_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (odm_condition_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) +MERGE (odm_condition_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) MERGE (odm_condition_value1)-[:HAS_FORMAL_EXPRESSION]->(odm_formal_expression1) MERGE (odm_condition_value2)-[:HAS_FORMAL_EXPRESSION]->(odm_formal_expression1) @@ -76,7 +85,10 @@ } AS final_properties MERGE (library:Library {name:"Sponsor", is_editable:true}) -MERGE (odm_description1:OdmDescription {name: "name1", language: "en", description: "description1", instruction: "instruction1", sponsor_instruction: "sponsor_instruction1"}) +MERGE (odm_translated_text1:OdmTranslatedText {text_type: "Description", language: "en", text: "Description1"}) +MERGE (odm_translated_text2:OdmTranslatedText {text_type: "osb:CompletionInstructions", language: "en", text: "Completion Instructions1"}) +MERGE (odm_translated_text3:OdmTranslatedText {text_type: "osb:DesignNotes", language: "en", text: "Design Notes1"}) +MERGE (odm_translated_text4:OdmTranslatedText {text_type: "osb:DisplayText", language: "en", text: "Display Text1"}) MERGE (library)-[:CONTAINS_CONCEPT]->(odm_method_root1:ConceptRoot:OdmMethodRoot {uid: "odm_method1"}) MERGE (odm_method_value1:ConceptValue:OdmMethodValue {oid: "oid1", name: "name1", method_type: "type1"}) @@ -92,8 +104,14 @@ MERGE (odm_method_root2)-[hv2:HAS_VERSION]->(odm_method_value2) SET hv2 = final_properties -MERGE (odm_method_value1)-[:HAS_DESCRIPTION]->(odm_description1) -MERGE (odm_method_value2)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (odm_method_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (odm_method_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (odm_method_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (odm_method_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (odm_method_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (odm_method_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (odm_method_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) +MERGE (odm_method_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) """ @@ -268,7 +286,10 @@ MERGE (library:Library {name:"Sponsor", is_editable:true}) -MERGE (odm_description1:OdmDescription {name: "name1", language: "eng", description: "description1", instruction: "instruction1", sponsor_instruction: "sponsor_instruction1"}) +MERGE (odm_translated_text1:OdmTranslatedText {text_type: "Description", language: "eng", text: "Description1"}) +MERGE (odm_translated_text2:OdmTranslatedText {text_type: "osb:DesignNotes", language: "eng", text: "Design Notes1"}) +MERGE (odm_translated_text3:OdmTranslatedText {text_type: "osb:CompletionInstructions", language: "eng", text: "Completion Instructions1"}) +MERGE (odm_translated_text4:OdmTranslatedText {text_type: "osb:DisplayText", language: "eng", text: "Display Text1"}) MERGE (item_group_root1:ConceptRoot:OdmItemGroupRoot {uid: "odm_item_group1"}) MERGE (item_group_value1:ConceptValue:OdmItemGroupValue {oid: "oid1", name: "name1", repeating: false, is_reference_data: false, sas_dataset_name: "sas_dataset_name1", origin: "origin1", purpose: "purpose1", comment: "comment1"}) @@ -276,7 +297,10 @@ MERGE (item_group_root1)-[r1:LATEST_FINAL]->(item_group_value1) MERGE (item_group_root1)-[hv2:HAS_VERSION]->(item_group_value1) MERGE (item_group_root1)-[:LATEST]->(item_group_value1) -MERGE (item_group_value1)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (item_group_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (item_group_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (item_group_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (item_group_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) SET hv2 = final_properties MERGE (item_group_root2:ConceptRoot:OdmItemGroupRoot {uid: "odm_item_group2"}) @@ -285,7 +309,10 @@ MERGE (item_group_root2)-[r2:LATEST_FINAL]->(item_group_value2) MERGE (item_group_root2)-[hv3:HAS_VERSION]->(item_group_value2) MERGE (item_group_root2)-[:LATEST]->(item_group_value2) -MERGE (item_group_value2)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (item_group_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (item_group_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (item_group_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (item_group_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) SET hv3 = final_properties // SDTM Domain codelist @@ -303,6 +330,9 @@ MERGE (domain_car)-[domain_carel:LATEST_FINAL]->(domain_cav) MERGE (domain_car)-[domain_carel_hv:HAS_VERSION]->(domain_cav) SET domain_carel_hv = final_properties +MERGE (Catalogue:CTCatalogue {name:"SDTM CT"}) +MERGE (library)-[:CONTAINS_CATALOUGE]->(Catalogue) +MERGE (Catalogue)-[:HAS_CODELIST]->(domain_codelist_root) // Create two terms in DOMAIN codelist MERGE (library)-[:CONTAINS_TERM]->(domain_tr:CTTermRoot {uid: "term_domain_xx"})-[:HAS_NAME_ROOT]-> @@ -354,7 +384,11 @@ MERGE (library:Library {name:"Sponsor", is_editable:true}) -MERGE (odm_description1:OdmDescription {name: "name1", language: "en", description: "description1", instruction: "instruction1", sponsor_instruction: "sponsor_instruction1"}) +MERGE (odm_translated_text1:OdmTranslatedText {text_type: "Description", language: "en", text: "Description1"}) +MERGE (odm_translated_text2:OdmTranslatedText {text_type: "osb:CompletionInstructions", language: "en", text: "Completion Instructions1"}) +MERGE (odm_translated_text3:OdmTranslatedText {text_type: "osb:DesignNotes", language: "en", text: "Design Notes1"}) +MERGE (odm_translated_text4:OdmTranslatedText {text_type: "osb:DisplayText", language: "en", text: "Display Text1"}) +MERGE (odm_translated_text5:OdmTranslatedText {text_type: "Question", language: "en", text: "Question1"}) MERGE (item_root1:ConceptRoot:OdmItemRoot {uid: "odm_item1"}) MERGE (item_value1:ConceptValue:OdmItemValue {oid: "oid1", name: "name1", datatype: "string", length: 1, significant_digits: 1, sas_field_name: "sasfieldname1", sds_var_name: "sdsvarname1", origin: "origin1", comment: "comment1"}) @@ -362,7 +396,11 @@ MERGE (item_root1)-[r1:LATEST_FINAL]->(item_value1) MERGE (item_root1)-[hv2:HAS_VERSION]->(item_value1) MERGE (item_root1)-[:LATEST]->(item_value1) -MERGE (item_value1)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (item_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (item_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (item_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (item_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) +MERGE (item_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text5) SET hv2 = final_properties MERGE (item_root2:ConceptRoot:OdmItemRoot {uid: "odm_item2"}) @@ -371,7 +409,11 @@ MERGE (item_root2)-[r2:LATEST_FINAL]->(item_value2) MERGE (item_root2)-[hv3:HAS_VERSION]->(item_value2) MERGE (item_root2)-[:LATEST]->(item_value2) -MERGE (item_value2)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (item_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (item_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (item_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (item_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) +MERGE (item_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text5) SET hv3 = final_properties """ @@ -385,7 +427,10 @@ } AS final_properties MERGE (library:Library {name:"Sponsor", is_editable:true}) -MERGE (odm_description1:OdmDescription {name: "name1", language: "en", description: "description1", instruction: "instruction1", sponsor_instruction: "sponsor_instruction1"}) +MERGE (odm_translated_text1:OdmTranslatedText {text_type: "Description", language: "en", text: "Description1"}) +MERGE (odm_translated_text2:OdmTranslatedText {text_type: "osb:CompletionInstructions", language: "en", text: "Completion Instructions1"}) +MERGE (odm_translated_text3:OdmTranslatedText {text_type: "osb:DesignNotes", language: "en", text: "Design Notes1"}) +MERGE (odm_translated_text4:OdmTranslatedText {text_type: "osb:DisplayText", language: "en", text: "Display Text1"}) MERGE (odm_alias1:OdmAlias {context: "context1", name: "name1"}) MERGE (odm_form_root1:ConceptRoot:OdmFormRoot {uid: "odm_form1"}) @@ -394,7 +439,10 @@ MERGE (odm_form_root1)-[r1:LATEST_FINAL]->(odm_form_value1) MERGE (odm_form_root1)-[hv2:HAS_VERSION]->(odm_form_value1) MERGE (odm_form_root1)-[:LATEST]->(odm_form_value1) -MERGE (odm_form_value1)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (odm_form_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (odm_form_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (odm_form_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (odm_form_value1)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) MERGE (odm_form_value1)-[:HAS_ALIAS]->(odm_alias1) SET hv2 = final_properties @@ -404,7 +452,10 @@ MERGE (odm_form_root2)-[r2:LATEST_FINAL]->(odm_form_value2) MERGE (odm_form_root2)-[hv3:HAS_VERSION]->(odm_form_value2) MERGE (odm_form_root2)-[:LATEST]->(odm_form_value2) -MERGE (odm_form_value2)-[:HAS_DESCRIPTION]->(odm_description1) +MERGE (odm_form_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text1) +MERGE (odm_form_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text2) +MERGE (odm_form_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text3) +MERGE (odm_form_value2)-[:HAS_TRANSLATED_TEXT]->(odm_translated_text4) MERGE (odm_form_value2)-[:HAS_ALIAS]->(odm_alias1) SET hv3 = final_properties """ @@ -446,7 +497,7 @@ } AS final_properties MERGE (Library:Library {name:"Sponsor", is_editable:true}) -MERGE (catalogue:CTCatalogue {name:"SDTM CT"}) +MERGE (Catalogue:CTCatalogue {name:"SDTM CT"}) MERGE (Library)-[:CONTAINS_CATALOUGE]->(Catalogue) WITH * @@ -467,7 +518,7 @@ MERGE (CodelistRoot:CTCodelistRoot {uid: "codelist_root1"}) MERGE (Library)-[:CONTAINS_CODELIST]->(CodelistRoot) MERGE (Catalogue)-[:HAS_CODELIST]->(CodelistRoot) -MERGE (ItemValue)-[:HAS_CODELIST]->(CodelistRoot) +MERGE (ItemValue)-[:HAS_CODELIST {allows_multi_choice: true}]->(CodelistRoot) WITH * MATCH (CTTerm:CTTermRoot {uid: "term1"}) @@ -475,7 +526,7 @@ MERGE (TermContext)-[:HAS_SELECTED_CODELIST]->(CodelistRoot) MERGE (CodelistRoot)-[:HAS_ATTRIBUTES_ROOT]->(CodelistAttrRoot:CTCodelistAttributesRoot) -MERGE (CodelistAttrValue:CTCodelistAttributesValue {name:"name1", definition:"definition1", preferred_term: "preferred_term1", synonyms: "synonyms1", submission_value: "submission_value1", extensible:false, ordinal: false}) +MERGE (CodelistAttrValue:CTCodelistAttributesValue {name:"name1", definition:"definition1", preferred_term: "preferred_term1", synonyms: "synonyms1", submission_value: "submission_value1", extensible:false, is_ordinal: false}) MERGE (CodelistAttrRoot)-[lf1:LATEST_FINAL]->(CodelistAttrValue) MERGE (CodelistAttrRoot)-[hv1:HAS_VERSION]->(CodelistAttrValue) MERGE (CodelistAttrRoot)-[:LATEST]->(CodelistAttrValue) @@ -564,7 +615,7 @@ MATCH (odm_vendor_namespace_root2:ConceptRoot:OdmVendorNamespaceRoot {uid: "odm_vendor_namespace2"})-[:LATEST]->(odm_vendor_namespace_value2:OdmVendorNamespaceValue) MERGE (odm_vendor_element_root1:ConceptRoot:OdmVendorElementRoot {uid: "odm_vendor_element1"}) -MERGE (odm_vendor_element_value1:ConceptValue:OdmVendorElementValue {name: "nameOne", compatible_types: '["FormDef","ItemGroupDef","ItemDef"]'}) +MERGE (odm_vendor_element_value1:ConceptValue:OdmVendorElementValue {name: "NameOne", compatible_types: '["FormDef","ItemGroupDef","ItemDef"]'}) MERGE (library)-[:CONTAINS_CONCEPT]->(odm_vendor_element_root1) MERGE (odm_vendor_element_root1)-[r1:LATEST_FINAL]->(odm_vendor_element_value1) MERGE (odm_vendor_element_root1)-[hv1:HAS_VERSION]->(odm_vendor_element_value1) @@ -573,7 +624,7 @@ SET hv1 = final_properties MERGE (odm_vendor_element_root2:ConceptRoot:OdmVendorElementRoot {uid: "odm_vendor_element2"}) -MERGE (odm_vendor_element_value2:ConceptValue:OdmVendorElementValue {name: "nameTwo", compatible_types: '["FormDef","ItemGroupDef","ItemDef"]'}) +MERGE (odm_vendor_element_value2:ConceptValue:OdmVendorElementValue {name: "NameTwo", compatible_types: '["FormDef","ItemGroupDef","ItemDef"]'}) MERGE (library)-[:CONTAINS_CONCEPT]->(odm_vendor_element_root2) MERGE (odm_vendor_element_root2)-[r2:LATEST_FINAL]->(odm_vendor_element_value2) MERGE (odm_vendor_element_root2)-[hv2:HAS_VERSION]->(odm_vendor_element_value2) @@ -582,7 +633,7 @@ SET hv2 = final_properties MERGE (odm_vendor_element_root3:ConceptRoot:OdmVendorElementRoot {uid: "odm_vendor_element3"}) -MERGE (odm_vendor_element_value3:ConceptValue:OdmVendorElementValue {name: "nameThree", compatible_types: '["FormDef","ItemGroupDef","ItemDef"]'}) +MERGE (odm_vendor_element_value3:ConceptValue:OdmVendorElementValue {name: "NameThree", compatible_types: '["FormDef","ItemGroupDef","ItemDef"]'}) MERGE (library)-[:CONTAINS_CONCEPT]->(odm_vendor_element_root3) MERGE (odm_vendor_element_root3)-[r3:LATEST_FINAL]->(odm_vendor_element_value3) MERGE (odm_vendor_element_root3)-[hv3:HAS_VERSION]->(odm_vendor_element_value3) @@ -591,7 +642,7 @@ SET hv3 = final_properties MERGE (odm_vendor_element_root4:ConceptRoot:OdmVendorElementRoot {uid: "odm_vendor_element4"}) -MERGE (odm_vendor_element_value4:ConceptValue:OdmVendorElementValue {name: "nameThree", compatible_types: '["NonCompatibleVendor"]'}) +MERGE (odm_vendor_element_value4:ConceptValue:OdmVendorElementValue {name: "NameThree", compatible_types: '["NonCompatibleVendor"]'}) MERGE (library)-[:CONTAINS_CONCEPT]->(odm_vendor_element_root4) MERGE (odm_vendor_element_root4)-[r4:LATEST_FINAL]->(odm_vendor_element_value4) MERGE (odm_vendor_element_root4)-[hv4:HAS_VERSION]->(odm_vendor_element_value4) @@ -1672,7 +1723,7 @@ MERGE (catalogue:CTCatalogue {name:"catalogue"})-[:HAS_CODELIST]-> (codelist_to_update:CTCodelistRoot {uid:"updated_codelist_uid"})-[:HAS_ATTRIBUTES_ROOT]-> (codelist_attr_root_to_update:CTCodelistAttributesRoot)-[final1:LATEST_FINAL]->(val1:CTCodelistAttributesValue -{name:"old_name", extensible:false, ordinal: false}) +{name:"old_name", extensible:false, is_ordinal: false}) MERGE (codelist_attr_root_to_update)-[final1hv:HAS_VERSION]->(val1) SET final1hv = old_props @@ -1683,7 +1734,7 @@ // Create a second codelist "deleted_codelist_uid", that will be deleted MERGE (catalogue)-[:HAS_CODELIST]->(codelist_to_delete:CTCodelistRoot {uid:"deleted_codelist_uid"})-[:HAS_ATTRIBUTES_ROOT]-> (codelist_attr_to_delete)-[final2:LATEST_FINAL]->(val2:CTCodelistAttributesValue -{name:"old_name", extensible:false, ordinal: false}) +{name:"old_name", extensible:false, is_ordinal: false}) MERGE (codelist_attr_to_delete)-[final2hv:HAS_VERSION]->(val2) SET final2hv=old_props @@ -1703,14 +1754,14 @@ // Update the codelist "updated_codelist_uid" MERGE (codelist_attr_root_to_update)-[final5:LATEST_FINAL]->(val5:CTCodelistAttributesValue -{name:"new_name", definition: "new_definition", extensible:false, ordinal: false}) +{name:"new_name", definition: "new_definition", extensible:false, is_ordinal: false}) MERGE (codelist_attr_root_to_update)-[final5hv:HAS_VERSION]->(val5) SET final5hv=new_props // Add a new codelist "added_codelist_uid" that is not available in the first package MERGE (catalogue)-[:HAS_CODELIST]->(codelist_to_add:CTCodelistRoot {uid:"added_codelist_uid"})-[:HAS_ATTRIBUTES_ROOT]-> (root6:CTCodelistAttributesRoot)-[final6:LATEST_FINAL]->(val6:CTCodelistAttributesValue -{name:"new_name", definition:"codelist_added", extensible:false, ordinal: false}) +{name:"new_name", definition:"codelist_added", extensible:false, is_ordinal: false}) MERGE (root6)-[final6hv:HAS_VERSION]->(val6) SET final6hv=new_props @@ -1769,7 +1820,7 @@ }) MERGE (old_package)-[:CONTAINS_CODELIST]->(package_codelist1:CTPackageCodelist)-[:CONTAINS_ATTRIBUTES]->(codelist_attr_value_to_update:CTCodelistAttributesValue -{name:"old_name", extensible:false, ordinal:false})<-[final1hv:HAS_VERSION]-(codelist_attr_root_to_update:CTCodelistAttributesRoot) +{name:"old_name", extensible:false, is_ordinal:false})<-[final1hv:HAS_VERSION]-(codelist_attr_root_to_update:CTCodelistAttributesRoot) <-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_update:CTCodelistRoot {uid:"updated_codelist_uid"}) SET final1hv = old_props MERGE (codelist_to_update)-[:HAS_NAME_ROOT]->(codelist_name_root_to_update:CTCodelistNameRoot)-[final2:LATEST_FINAL]->(codelist_name_value_to_update:CTCodelistNameValue) @@ -1777,7 +1828,7 @@ MERGE (codelist_name_root_to_update)-[final2hv:HAS_VERSION]->(codelist_name_value_to_update) SET final2hv=old_props MERGE (old_package)-[:CONTAINS_CODELIST]->(package_codelist2:CTPackageCodelist)-[:CONTAINS_ATTRIBUTES]->(codelist_attributes_value_to_delete:CTCodelistAttributesValue -{name:"old_name", extensible:false, ordinal:false})<-[final3:LATEST_FINAL]-(codelist_attributes_root_to_delete:CTCodelistAttributesRoot)<-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_delete:CTCodelistRoot {uid:"deleted_codelist_uid"}) +{name:"old_name", extensible:false, is_ordinal:false})<-[final3:LATEST_FINAL]-(codelist_attributes_root_to_delete:CTCodelistAttributesRoot)<-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_delete:CTCodelistRoot {uid:"deleted_codelist_uid"}) MERGE (codelist_attributes_root_to_delete)-[final3hv:HAS_VERSION]->(codelist_attributes_value_to_delete) SET final3hv=old_props MERGE (package_codelist1)-[contains_term:CONTAINS_TERM]->(package_term1:CTPackageTerm)-[:CONTAINS_ATTRIBUTES]->(term_attr_value_to_update:CTTermAttributesValue @@ -1797,11 +1848,11 @@ SET final6hv=old_props MERGE (new_package)-[:CONTAINS_CODELIST]->(package_codelist3:CTPackageCodelist)-[:CONTAINS_ATTRIBUTES]->(attr_val7:CTCodelistAttributesValue -{name:"new_name", definition: "new_definition", extensible:false, ordinal:false})<-[final7:LATEST_FINAL]-(codelist_attr_root_to_update) +{name:"new_name", definition: "new_definition", extensible:false, is_ordinal:false})<-[final7:LATEST_FINAL]-(codelist_attr_root_to_update) MERGE (codelist_attr_root_to_update)-[final7hv:HAS_VERSION]->(attr_val7) SET final7hv = new_props MERGE (new_package)-[:CONTAINS_CODELIST]->(package_codelist4:CTPackageCodelist)-[:CONTAINS_ATTRIBUTES]->(attr_val8:CTCodelistAttributesValue -{name:"new_name", definition:"codelist_added", extensible:false, ordinal:false})<-[final8:LATEST_FINAL]-(attr_root8:CTCodelistAttributesRoot)<-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_add:CTCodelistRoot {uid:"added_codelist_uid"}) +{name:"new_name", definition:"codelist_added", extensible:false, is_ordinal:false})<-[final8:LATEST_FINAL]-(attr_root8:CTCodelistAttributesRoot)<-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_add:CTCodelistRoot {uid:"added_codelist_uid"}) MERGE (attr_root8)-[final8hv:HAS_VERSION]->(attr_val8) SET final8hv = new_props MERGE (attr_root8)-[:LATEST]->(attr_val8) @@ -1837,7 +1888,7 @@ uid:"package1_uid",name:"package1",effective_date:date("2020-06-26")}) -[:CONTAINS_CODELIST]->(p_codelist1:CTPackageCodelist {uid:"package1_uid_cdlist_code1"}) -[:CONTAINS_ATTRIBUTES]->(:CTCodelistAttributesValue -{name:"codelist_name1", extensible:false, ordinal:false, submission_value:"submission_value1", definition: "definition1", +{name:"codelist_name1", extensible:false, is_ordinal:false, submission_value:"submission_value1", definition: "definition1", preferred_term:"codelist_pref_term1", synonyms:apoc.text.split("synonym1",",")}) MERGE (p_codelist1)-[:CONTAINS_TERM]->(pt1:CTPackageTerm)-[:CONTAINS_ATTRIBUTES]->(:CTTermAttributesValue @@ -1850,7 +1901,7 @@ uid:"package2_uid",name:"package2",effective_date:date("2020-06-26")}) -[:CONTAINS_CODELIST]->(p_codelist2:CTPackageCodelist {uid:"package2_uid_cdlist_code2"}) -[:CONTAINS_ATTRIBUTES]->(:CTCodelistAttributesValue -{name:"codelist_name2", extensible:false, ordinal:false, submission_value:"submission_value2", definition: "definition2", +{name:"codelist_name2", extensible:false, is_ordinal:false, submission_value:"submission_value2", definition: "definition2", preferred_term:"codelist_pref_term2", synonyms:apoc.text.split("synonym2",",")}) MERGE (p_codelist2)-[:CONTAINS_TERM]->(pt2:CTPackageTerm)-[:CONTAINS_ATTRIBUTES]->(:CTTermAttributesValue @@ -1884,7 +1935,7 @@ }) MERGE (old_package)-[:CONTAINS_CODELIST]->(package_codelist1:CTPackageCodelist {uid:"old_package_uid_codelist_code1"})-[:CONTAINS_ATTRIBUTES]->(cav1:CTCodelistAttributesValue -{name:"old_name1", extensible:false, ordinal:false, submission_value:"old_submission_value1", definition:"old_definition1", preferred_term:"old_pref_term1", synonyms:apoc.text.split("syn1,syn2",",")}) +{name:"old_name1", extensible:false, is_ordinal:false, submission_value:"old_submission_value1", definition:"old_definition1", preferred_term:"old_pref_term1", synonyms:apoc.text.split("syn1,syn2",",")}) <-[final1:LATEST_FINAL]-(codelist_attr_root_to_update:CTCodelistAttributesRoot) <-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_update:CTCodelistRoot {uid:"updated_codelist_uid"}) MERGE (codelist_attr_root_to_update)-[hv1:HAS_VERSION]->(cav1) @@ -1894,7 +1945,7 @@ SET hv2=old_props MERGE (old_package)-[:CONTAINS_CODELIST]->(package_codelist2:CTPackageCodelist {uid:"old_package_uid_codelist_code2"})-[:CONTAINS_ATTRIBUTES]->(cav3:CTCodelistAttributesValue -{name:"old_name2", extensible:false, ordinal:false, submission_value:"old_submission_value2", definition: "old_definition2", preferred_term:"old_pref_term2", synonyms:apoc.text.split("synonym",",")}) +{name:"old_name2", extensible:false, is_ordinal:false, submission_value:"old_submission_value2", definition: "old_definition2", preferred_term:"old_pref_term2", synonyms:apoc.text.split("synonym",",")}) <-[final3:LATEST_FINAL]-(car3:CTCodelistAttributesRoot)<-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_delete:CTCodelistRoot {uid:"deleted_codelist_uid"}) MERGE (car3)-[hv3:HAS_VERSION]->(cav3) SET hv3=old_props @@ -1926,13 +1977,13 @@ MERGE (new_package)-[:CONTAINS_CODELIST]->(package_codelist3:CTPackageCodelist {uid:"new_package_uid_codelist_code3"})-[:CONTAINS_ATTRIBUTES]->(cav7:CTCodelistAttributesValue -{name:"new_name", definition: "new_definition", extensible:true, ordinal:false, submission_value:"new_submission_value", preferred_term:"new_pref_term1"}) +{name:"new_name", definition: "new_definition", extensible:true, is_ordinal:false, submission_value:"new_submission_value", preferred_term:"new_pref_term1"}) <-[final7:LATEST_FINAL]-(codelist_attr_root_to_update) MERGE (codelist_attr_root_to_update)-[hv7:HAS_VERSION]->(cav7) SET hv7 = new_props MERGE (new_package)-[:CONTAINS_CODELIST]->(package_codelist4:CTPackageCodelist {uid:"new_package_uid_codelist_code4"})-[:CONTAINS_ATTRIBUTES]->(cav8:CTCodelistAttributesValue -{name:"new_name", submission_value:"new_submission_value",definition:"codelist_added", extensible:false, ordinal:false, preferred_term:"new_pref_term", synonyms:apoc.text.split("syn1,syn2,syn3",",")}) +{name:"new_name", submission_value:"new_submission_value",definition:"codelist_added", extensible:false, is_ordinal:false, preferred_term:"new_pref_term", synonyms:apoc.text.split("syn1,syn2,syn3",",")}) <-[final8:LATEST_FINAL]-(car8:CTCodelistAttributesRoot)<-[:HAS_ATTRIBUTES_ROOT]-(codelist_to_add:CTCodelistRoot {uid:"added_codelist_uid"}) MERGE (car8)-[hv8:HAS_VERSION]->(cav8) SET hv8 = new_props @@ -1965,7 +2016,7 @@ preferred_term: "codelist preferred term", definition: "codelist definition", extensible: false, - ordinal: false}) + is_ordinal: false}) MERGE (:CTTermRoot {uid:"ct_term_root1"}) MERGE (cc:CTCatalogue {name: "SDTM CT"}) MERGE (cc)-[:HAS_CODELIST]->(cr) @@ -1993,7 +2044,7 @@ preferred_term: "codelist preferred term", definition: "codelist definition", extensible: false, - ordinal: false}) + is_ordinal: false}) MERGE (cc)-[:HAS_CODELIST]->(cr2) MERGE (car2)-[hv3:HAS_VERSION]->(cav2) CREATE (car2)-[hv4:HAS_VERSION]->(cav2) @@ -2019,7 +2070,7 @@ preferred_term: "codelist preferred term", definition: "codelist definition", extensible: false, - ordinal: false}) + is_ordinal: false}) MERGE (cc)-[:HAS_CODELIST]->(cr3) MERGE (car3)-[ld3:LATEST_DRAFT]->(cav3) MERGE (car3)-[hv5:HAS_VERSION]->(cav3) @@ -2118,7 +2169,7 @@ MERGE (cc:CTCatalogue {name: "SDTM CT"}) MERGE (cc)-[:HAS_CODELIST]->(cr:CTCodelistRoot {uid:"editable_cr"})-[:HAS_NAME_ROOT] ->(codelist_ver_root:CTCodelistNameRoot)-[:HAS_VERSION{change_description: "Approved version",start_date: datetime(),status: "Final",author_id: "TODO initials",version : "1.0"}]->(codelist_ver_value:CTCodelistNameValue {name: "Objective Level", name_sentence_case: "objective level"}) -MERGE (cr)-[:HAS_ATTRIBUTES_ROOT]->(car:CTCodelistAttributesRoot)-[:LATEST]->(cav:CTCodelistAttributesValue {name: "codelist attributes value1", submission_value: "codelist submission value1", preferred_term: "codelist preferred term", definition: "codelist definition", extensible: true, ordinal: false}) +MERGE (cr)-[:HAS_ATTRIBUTES_ROOT]->(car:CTCodelistAttributesRoot)-[:LATEST]->(cav:CTCodelistAttributesValue {name: "codelist attributes value1", submission_value: "codelist submission value1", preferred_term: "codelist preferred term", definition: "codelist definition", extensible: true, is_ordinal: false}) CREATE (codelist_ver_root)-[:LATEST]->(codelist_ver_value) CREATE (codelist_ver_root)-[:LATEST_FINAL]->(codelist_ver_value) @@ -2262,7 +2313,7 @@ MERGE (cc:CTCatalogue {name: "SDTM CT"}) MERGE (cc)-[:HAS_CODELIST]->(cr:CTCodelistRoot {uid:"domain_cl"})-[:HAS_NAME_ROOT] ->(codelist_ver_root:CTCodelistNameRoot)-[:HAS_VERSION{change_description: "Approved version",start_date: datetime(),status: "Final",author_id: "TODO initials",version : "1.0"}]->(codelist_ver_value:CTCodelistNameValue {name: "SDTM Domain Abbreviation", name_sentence_case: "SDTM domain abbreviation"}) -MERGE (cr)-[:HAS_ATTRIBUTES_ROOT]->(car:CTCodelistAttributesRoot)-[:LATEST]->(cav:CTCodelistAttributesValue {name: "SDTM domain abbreviation", submission_value: "DOMAIN", preferred_term: "SDTM Domain Abbreviation", definition: "domain codelist definition", extensible: true, ordinal: false}) +MERGE (cr)-[:HAS_ATTRIBUTES_ROOT]->(car:CTCodelistAttributesRoot)-[:LATEST]->(cav:CTCodelistAttributesValue {name: "SDTM domain abbreviation", submission_value: "DOMAIN", preferred_term: "SDTM Domain Abbreviation", definition: "domain codelist definition", extensible: true, is_ordinal: false}) CREATE (codelist_ver_root)-[:LATEST]->(codelist_ver_value) CREATE (codelist_ver_root)-[:LATEST_FINAL]->(codelist_ver_value) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/factory_soa.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/factory_soa.py index 1d29f160..5ed09b3f 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/factory_soa.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/factory_soa.py @@ -397,8 +397,8 @@ class SoATestData: NUM_SOA_ROWS = 43 NUM_ACTIVITY_REQUEST_ROWS = 3 + 3 + 3 # 3 activity requests with their groupings NUM_ACTIVITY_INSTANCES = 8 - # Placeholders (activity requests without instances) are not shown in operational SoA - # because they're filtered out in _build_flowchart_table when activity_instance is None + # Operational SoA XLSX includes all activities (17) plus their instances (8) + NUM_OPERATIONAL_SOA_XLSX_DATA_ROWS = 25 NUM_OPERATIONAL_SOA_ROWS = NUM_SOA_ROWS + NUM_ACTIVITY_INSTANCES NUM_OPERATIONAL_SOA_SCHEDULES = 9 # Mind that study-activities are scheduled, not study-activity-instances, there may be multiple instances per activity NUM_OPERATIONAL_SOA_CHECKMARKS = ( diff --git a/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/utils.py b/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/utils.py index 286e5129..15764878 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/utils.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/integration/utils/utils.py @@ -23,7 +23,11 @@ DatasetVariable, VariableClass, ) -from clinical_mdr_api.domains.enums import StudyDesignClassEnum, StudySourceVariableEnum +from clinical_mdr_api.domains.enums import ( + LibraryItemStatus, + StudyDesignClassEnum, + StudySourceVariableEnum, +) from clinical_mdr_api.main import app from clinical_mdr_api.models.biomedical_concepts.activity_instance_class import ( ActivityInstanceClass, @@ -86,10 +90,19 @@ ) from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmAliasModel, - OdmDescriptionModel, + OdmFormalExpressionModel, + OdmTranslatedTextModel, +) +from clinical_mdr_api.models.concepts.odms.odm_condition import ( + OdmCondition, + OdmConditionPostInput, ) from clinical_mdr_api.models.concepts.odms.odm_form import OdmForm, OdmFormPostInput -from clinical_mdr_api.models.concepts.odms.odm_item import OdmItem, OdmItemPostInput +from clinical_mdr_api.models.concepts.odms.odm_item import ( + OdmItem, + OdmItemCodelist, + OdmItemPostInput, +) from clinical_mdr_api.models.concepts.odms.odm_item_group import ( OdmItemGroup, OdmItemGroupPostInput, @@ -98,6 +111,18 @@ OdmStudyEvent, OdmStudyEventPostInput, ) +from clinical_mdr_api.models.concepts.odms.odm_vendor_attribute import ( + OdmVendorAttribute, + OdmVendorAttributePostInput, +) +from clinical_mdr_api.models.concepts.odms.odm_vendor_element import ( + OdmVendorElement, + OdmVendorElementPostInput, +) +from clinical_mdr_api.models.concepts.odms.odm_vendor_namespace import ( + OdmVendorNamespace, + OdmVendorNamespacePostInput, +) from clinical_mdr_api.models.concepts.pharmaceutical_product import ( PharmaceuticalProduct, PharmaceuticalProductCreateInput, @@ -348,12 +373,22 @@ from clinical_mdr_api.services.concepts.medicinal_products_service import ( MedicinalProductService, ) +from clinical_mdr_api.services.concepts.odms.odm_conditions import OdmConditionService from clinical_mdr_api.services.concepts.odms.odm_forms import OdmFormService from clinical_mdr_api.services.concepts.odms.odm_item_groups import OdmItemGroupService from clinical_mdr_api.services.concepts.odms.odm_items import OdmItemService from clinical_mdr_api.services.concepts.odms.odm_study_events import ( OdmStudyEventService, ) +from clinical_mdr_api.services.concepts.odms.odm_vendor_attributes import ( + OdmVendorAttributeService, +) +from clinical_mdr_api.services.concepts.odms.odm_vendor_elements import ( + OdmVendorElementService, +) +from clinical_mdr_api.services.concepts.odms.odm_vendor_namespaces import ( + OdmVendorNamespaceService, +) from clinical_mdr_api.services.concepts.pharmaceutical_products_service import ( PharmaceuticalProductService, ) @@ -529,6 +564,7 @@ from common.auth.dependencies import dummy_user_test_auth from common.auth.user import clear_users_cache from common.config import settings +from common.utils import VisitClass, VisitSubclass log = logging.getLogger(__name__) @@ -1999,7 +2035,7 @@ def create_study_visit( visit_type_uid: str, show_visit: bool, visit_contact_mode_uid: str, - visit_class: str, + visit_class: VisitClass, is_global_anchor_visit: bool, time_reference_uid: str | None = None, time_value: int | None = None, @@ -2012,7 +2048,7 @@ def create_study_visit( start_rule: str | None = None, end_rule: str | None = None, epoch_allocation_uid: str | None = None, - visit_subclass: str | None = None, + visit_subclass: VisitSubclass | None = None, ) -> StudyVisit: service: StudyVisitService = StudyVisitService(study_uid=study_uid) study_visit_input: StudyVisitCreateInput = StudyVisitCreateInput( @@ -2313,12 +2349,12 @@ def create_odm_form( oid=None, repeating="Yes", sdtm_version=None, - descriptions: list[OdmDescriptionModel] | None = None, + translated_texts: list[OdmTranslatedTextModel] | None = None, aliases: list[OdmAliasModel] | None = None, approve: bool = True, ) -> OdmForm: - if not descriptions: - descriptions = [] + if not translated_texts: + translated_texts = [] if not aliases: aliases = [] @@ -2330,7 +2366,7 @@ def create_odm_form( oid=cls.random_if_none(oid), repeating=repeating, sdtm_version=cls.random_if_none(sdtm_version), - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, ) @@ -2351,13 +2387,13 @@ def create_odm_item_group( origin=None, purpose=None, comment=None, - descriptions: list[OdmDescriptionModel] | None = None, + translated_texts: list[OdmTranslatedTextModel] | None = None, aliases: list[OdmAliasModel] | None = None, sdtm_domain_uids=None, approve: bool = True, ) -> OdmItemGroup: - if not descriptions: - descriptions = [] + if not translated_texts: + translated_texts = [] if not aliases: aliases = [] if not sdtm_domain_uids: @@ -2375,7 +2411,7 @@ def create_odm_item_group( origin=cls.random_if_none(origin), purpose=cls.random_if_none(purpose), comment=cls.random_if_none(comment), - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, sdtm_domain_uids=sdtm_domain_uids, ) @@ -2399,17 +2435,17 @@ def create_odm_item( sds_var_name=None, origin=None, comment=None, - descriptions: list[OdmDescriptionModel] | None = None, + translated_texts: list[OdmTranslatedTextModel] | None = None, aliases: list[OdmAliasModel] | None = None, - codelist_uid=None, + codelist: OdmItemCodelist | None = None, unit_definitions=None, terms=None, approve: bool = True, ) -> OdmItem: if not terms: terms = [] - if not descriptions: - descriptions = [] + if not translated_texts: + translated_texts = [] if not unit_definitions: unit_definitions = [] if not aliases: @@ -2429,9 +2465,9 @@ def create_odm_item( sds_var_name=cls.random_if_none(sds_var_name), origin=cls.random_if_none(origin), comment=cls.random_if_none(comment), - descriptions=descriptions, + translated_texts=translated_texts, aliases=aliases, - codelist_uid=codelist_uid, + codelist=codelist, unit_definitions=unit_definitions, terms=terms, ) @@ -2441,6 +2477,121 @@ def create_odm_item( service.approve(result.uid) return result + @classmethod + def create_odm_condition( + cls, + name=None, + library_name=LIBRARY_NAME, + oid=None, + formal_expressions: list[OdmFormalExpressionModel] | None = None, + translated_texts: list[OdmTranslatedTextModel] | None = None, + aliases: list[OdmAliasModel] | None = None, + approve: bool = True, + ) -> OdmCondition: + if not formal_expressions: + formal_expressions = [] + if not translated_texts: + translated_texts = [] + if not aliases: + aliases = [] + + service: OdmConditionService = OdmConditionService() + + payload: OdmConditionPostInput = OdmConditionPostInput( + library_name=library_name, + name=cls.random_if_none(name), + oid=cls.random_if_none(oid), + formal_expressions=formal_expressions, + translated_texts=translated_texts, + aliases=aliases, + ) + + result: OdmCondition = service.create(concept_input=payload) # type: ignore[assignment] + if approve: + service.approve(result.uid) + return result + + @classmethod + def create_odm_vendor_namespace( + cls, + name=None, + library_name=LIBRARY_NAME, + prefix=None, + url=None, + approve: bool = True, + ) -> OdmVendorNamespace: + service: OdmVendorNamespaceService = OdmVendorNamespaceService() + + payload: OdmVendorNamespacePostInput = OdmVendorNamespacePostInput( + library_name=library_name, + name=cls.random_if_none(name), + prefix=prefix, + url=url, + ) + + result: OdmVendorNamespace = service.create(concept_input=payload) # type: ignore[assignment] + if approve: + service.approve(result.uid) + return result + + @classmethod + def create_odm_vendor_element( + cls, + name=None, + library_name=LIBRARY_NAME, + compatible_types=None, + vendor_namespace_uid=None, + approve: bool = True, + ) -> OdmVendorElement: + if not compatible_types: + compatible_types = [] + + service: OdmVendorElementService = OdmVendorElementService() + + payload: OdmVendorElementPostInput = OdmVendorElementPostInput( + library_name=library_name, + name=cls.random_if_none(name), + compatible_types=compatible_types, + vendor_namespace_uid=vendor_namespace_uid, + ) + + result: OdmVendorElement = service.create(concept_input=payload) # type: ignore[assignment] + if approve: + service.approve(result.uid) + return result + + @classmethod + def create_odm_vendor_attribute( + cls, + name=None, + library_name=LIBRARY_NAME, + compatible_types=None, + data_type=None, + value_regex=None, + vendor_namespace_uid=None, + vendor_element_uid=None, + approve: bool = True, + ) -> OdmVendorAttribute: + if not compatible_types: + compatible_types = [] + + service: OdmVendorAttributeService = OdmVendorAttributeService() + + payload: OdmVendorAttributePostInput = OdmVendorAttributePostInput( + library_name=library_name, + name=cls.random_if_none(name), + compatible_types=compatible_types, + data_type=data_type, + value_regex=value_regex, + vendor_namespace_uid=vendor_namespace_uid, + vendor_element_uid=vendor_element_uid, + ) + + result: OdmVendorAttribute = service.create(concept_input=payload) # type: ignore[assignment] + if approve: + service.approve(result.uid) + return result + @classmethod def create_library( cls, name: str = LIBRARY_NAME, is_editable: bool = True @@ -2530,6 +2681,7 @@ def create_ct_term( sponsor_preferred_name: str | None = None, sponsor_preferred_name_sentence_case: str | None = None, order: int | None = None, + ordinal: float | None = None, library_name: str = CT_CODELIST_LIBRARY_SPONSOR, approve: bool = True, effective_date: datetime | None = None, @@ -2544,6 +2696,7 @@ def create_ct_term( codelist_uid=codelist_uid, submission_value=submission_value, order=order, + ordinal=ordinal, ) ] payload = CTTermCreateInput( @@ -2731,7 +2884,7 @@ def create_ct_codelists_using_cypher(cls): name_sentence_case: $uid + 'name'}) MERGE (codelist_root)-[:HAS_ATTRIBUTES_ROOT]->(codelist_a_root:CTCodelistAttributesRoot) -[:LATEST]->(codelist_a_value:CTCodelistAttributesValue {definition:$uid + ' DEF', - name:$uid + ' NAME', preferred_term:$uid + ' PREF', submission_value:$submission_value, extensible:true, ordinal:false}) + name:$uid + ' NAME', preferred_term:$uid + ' PREF', submission_value:$submission_value, extensible:true, is_ordinal:false}) MERGE (catalogue)-[:HAS_CODELIST]->(codelist_root) MERGE (codelist_ver_root)-[name_final:LATEST_FINAL]->(codelist_ver_value) MERGE (codelist_ver_root)-[name_hasver:HAS_VERSION]->(codelist_ver_value) @@ -2761,6 +2914,7 @@ def create_ct_codelist( sponsor_preferred_name: str | None = None, definition: str | None = None, extensible: bool = False, + is_ordinal: bool = False, template_parameter: bool = False, parent_codelist_uid: str | None = None, terms: list[CTCodelistTermInput] | None = None, @@ -2785,7 +2939,7 @@ def create_ct_codelist( ), definition=cls.random_if_none(definition, prefix="definition-"), extensible=extensible, - ordinal=False, + is_ordinal=is_ordinal, sponsor_preferred_name=cls.random_if_none( sponsor_preferred_name, prefix="name-" ), @@ -3040,7 +3194,7 @@ def get_unit_by_uid( return UnitDefinitionService().get_by_uid( uid=unit_uid, at_specific_date=at_specified_datetime, - status=status, + status=LibraryItemStatus(status) if status is not None else None, version=version, ) @@ -4005,6 +4159,7 @@ def create_study_arm( study_uid: str, name: str, short_name: str, + label: str | None = None, code: str | None = None, description: str | None = None, randomization_group: str | None = None, @@ -4015,6 +4170,7 @@ def create_study_arm( arm_input = StudySelectionArmCreateInput( name=name, short_name=short_name, + label=label, code=code, description=description, randomization_group=randomization_group, diff --git a/clinical-mdr-api/clinical_mdr_api/tests/unit/domain/controlled_terminology_aggregates/test_ct_codelist_attributes.py b/clinical-mdr-api/clinical_mdr_api/tests/unit/domain/controlled_terminology_aggregates/test_ct_codelist_attributes.py index 807f3271..879a8f19 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/unit/domain/controlled_terminology_aggregates/test_ct_codelist_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/unit/domain/controlled_terminology_aggregates/test_ct_codelist_attributes.py @@ -26,7 +26,7 @@ def create_random_ct_codelist_attributes_vo( preferred_term=random_str(), definition=random_str(), extensible=True, - ordinal=False, + is_ordinal=False, ) return random_ct_codelist_attributes_vo diff --git a/clinical-mdr-api/clinical_mdr_api/tests/unit/models/test_table_f.py b/clinical-mdr-api/clinical_mdr_api/tests/unit/models/test_table_f.py index 080f4e4c..ce7084c7 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/unit/models/test_table_f.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/unit/models/test_table_f.py @@ -17,6 +17,7 @@ TableWithFootnotes, table_to_docx, table_to_html, + table_to_xlsx, tables_to_html, ) @@ -527,3 +528,31 @@ def compare_docx_footnotes( assert ( textx == footnote.text_plain ), f"footnote text doesn't match in row {row_idx}" + + +def test_table_to_xlsx(): + workbook = table_to_xlsx(TEST_TABLE) + worksheet = workbook.active + + assert worksheet.title == TEST_TABLE.title + assert worksheet.max_row == len(TEST_TABLE.rows) + assert worksheet.max_column == len(TEST_TABLE.rows[0].cells) + assert worksheet.freeze_panes == "C4" + + for r, row in enumerate(TEST_TABLE.rows, start=1): + for c, cell in enumerate(row.cells, start=1): + value = worksheet.cell(row=r, column=c).value + if value is None: + value = "" + assert value == cell.text + + expected_merged = { + "B1:C1", + "B2:C2", + "B3:C3", + "B5:C5", + "B6:C6", + "A7:B7", + } + merged_ranges = {str(rng) for rng in worksheet.merged_cells.ranges} + assert expected_merged.issubset(merged_ranges) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/unit/services/test_study.py b/clinical-mdr-api/clinical_mdr_api/tests/unit/services/test_study.py index d4f6cb3b..6a958fee 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/unit/services/test_study.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/unit/services/test_study.py @@ -1,5 +1,4 @@ import random -import sys import unittest from dataclasses import dataclass from typing import Iterable @@ -583,7 +582,7 @@ def test__study_service__create__success( # then another_repo_instance = StudyDefinitionRepositoryFake(test_db) db_content = another_repo_instance.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items self.assertEqual(len(db_content), 1) for study_definition_ar in db_content: @@ -959,7 +958,7 @@ def test__patch__high_level_study_design__success( # then another_repo_instance = StudyDefinitionRepositoryFake(test_db) db_content = another_repo_instance.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items self.assertEqual(len(db_content), 1) @@ -1084,7 +1083,7 @@ def test__patch__id_metadata__success( # then another_repo_instance = StudyDefinitionRepositoryFake(test_db) db_content = another_repo_instance.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items self.assertEqual(len(db_content), 1) @@ -1321,7 +1320,7 @@ def test__patch__id_metadata_registry_identifiers__success( # then another_repo_instance = StudyDefinitionRepositoryFake(test_db) db_content = another_repo_instance.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items self.assertEqual(len(db_content), 1) @@ -1582,7 +1581,7 @@ def test__patch__study_population__success( # then another_repo_instance = StudyDefinitionRepositoryFake(test_db) db_content = another_repo_instance.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items self.assertEqual(len(db_content), 1) @@ -1739,7 +1738,7 @@ def test__patch__study_intervention__success( # then another_repo_instance = StudyDefinitionRepositoryFake(test_db) db_content = another_repo_instance.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items self.assertEqual(len(db_content), 1) @@ -1887,7 +1886,7 @@ def test__patch__study_description__success( # then another_repo_instance = StudyDefinitionRepositoryFake(test_db) db_content = another_repo_instance.find_all( - page_number=1, page_size=sys.maxsize + page_number=1, page_size=settings.max_int_neo4j - 1 ).items self.assertEqual(len(db_content), 1) diff --git a/clinical-mdr-api/clinical_mdr_api/tests/utils/utils.py b/clinical-mdr-api/clinical_mdr_api/tests/utils/utils.py index 43f4e6e8..3448da6f 100644 --- a/clinical-mdr-api/clinical_mdr_api/tests/utils/utils.py +++ b/clinical-mdr-api/clinical_mdr_api/tests/utils/utils.py @@ -2,41 +2,49 @@ from xml.etree.ElementTree import Element -def xml_diff(expected: Element, actual: Element, path: str = "Root"): +def xml_diff(source: Element, target: Element, path: str = "Root"): """ Compare two XML documents. Order of tags and attributes matters. """ - assert expected.tag == actual.tag, ( + assert source.tag == target.tag, ( f"\nPATH: {path}\n" - f"EXPECTED tag: {expected.tag}\n" - f"ACTUAL tag: {actual.tag}\n\n\n" + f"SOURCE tag: {source.tag}\n" + f"TARGET tag: {target.tag}\n\n\n" ) - if isinstance(expected.text, str) and isinstance(actual.text, str): - expected_text = expected.text.strip() - actual_text = actual.text.strip() - assert expected_text == actual_text, ( + if isinstance(source.text, str) and isinstance(target.text, str): + source_text = source.text.strip() + target_text = target.text.strip() + assert source_text == target_text, ( f"\nPATH: {path}\n" - f"Values of {expected.tag} don't match:\n" - f"EXPECTED: {expected_text}\n" - f"ACTUAL: {actual_text}\n\n\n" + f"Values of {source.tag} don't match:\n" + f"SOURCE: {source_text}\n" + f"TARGET: {target_text}\n\n\n" ) - assert set(expected.items()) == set(actual.items()), ( + assert set(source.items()) == set(target.items()), ( f"\nPATH: {path}\n" - f"Attributes of {expected.tag} don't match:\n" - f"EXPECTED: {expected.items()}\n" - f"ACTUAL: {actual.items()}\n\n\n" + f"Attributes of {source.tag} don't match:\n" + f"SOURCE: {source.items()}\n" + f"TARGET: {target.items()}\n\n\n" ) - expected_sub_elements = list(expected) - actual_sub_elements = list(actual) + source_sub_elements = list(source) + target_sub_elements = list(target) - for idx, elm in enumerate(actual_sub_elements): - xml_diff( - expected_sub_elements[idx], - elm, - f"{path}->{idx}:{expected_sub_elements[idx].tag}", - ) + for idx, elm in enumerate(target_sub_elements): + try: + xml_diff( + source_sub_elements[idx], + elm, + f"{path}->{idx}:{source_sub_elements[idx].tag}", + ) + except IndexError as e: + raise AssertionError( + f"\nPATH: {path}\n" + f"SOURCE has fewer elements than TARGET at index {idx}:\n" + f"SOURCE: {len(source_sub_elements)} elements\n" + f"TARGET: {len(target_sub_elements)} elements\n\n\n" + ) from e # remove specified keys from a dictionary diff --git a/clinical-mdr-api/common/auth/rbac.py b/clinical-mdr-api/common/auth/rbac.py index 4e048677..3445975a 100644 --- a/clinical-mdr-api/common/auth/rbac.py +++ b/clinical-mdr-api/common/auth/rbac.py @@ -13,6 +13,7 @@ LIBRARY_WRITE_OR_STUDY_WRITE = Depends( RequiresAnyRole({"Library.Write", "Study.Write"}) ) +LIBRARY_READ_OR_STUDY_READ = Depends(RequiresAnyRole({"Library.Read", "Study.Read"})) ANY = Depends( RequiresAnyRole({"Library.Write", "Study.Write", "Library.Read", "Study.Read"}) ) diff --git a/clinical-mdr-api/common/tests/unit/test_utils.py b/clinical-mdr-api/common/tests/unit/test_utils.py index b045e011..2bd6da6d 100644 --- a/clinical-mdr-api/common/tests/unit/test_utils.py +++ b/clinical-mdr-api/common/tests/unit/test_utils.py @@ -33,7 +33,7 @@ def test_strtobool(): @pytest.mark.parametrize( "page_number, page_size", - [[1, 10], [2, 200], [3000, 1000], [settings.max_int_neo4j, 1]], + [[1, 10], [2, 200], [3000, 1000], [settings.max_int_neo4j - 1, 1]], ) def test_validate_page_number_and_page_size(page_number, page_size): validate_page_number_and_page_size(page_number, page_size) @@ -42,9 +42,9 @@ def test_validate_page_number_and_page_size(page_number, page_size): @pytest.mark.parametrize( "page_number, page_size", [ - [settings.max_int_neo4j + 1, 1], + [settings.max_int_neo4j, 1], [settings.max_int_neo4j, 10], - [1, settings.max_int_neo4j + 1], + [1, settings.max_int_neo4j], [10, settings.max_int_neo4j], ], ) @@ -53,7 +53,7 @@ def test_validate_page_number_and_page_size_negative(page_number, page_size): validate_page_number_and_page_size(page_number, page_size) assert ( str(exc_info.value) - == f"(page_number * page_size) value cannot be bigger than {settings.max_int_neo4j}" + == f"(page_number * page_size) value must be smaller than {settings.max_int_neo4j}" ) diff --git a/clinical-mdr-api/common/utils.py b/clinical-mdr-api/common/utils.py index 16a9a6e9..c0f32b1b 100644 --- a/clinical-mdr-api/common/utils.py +++ b/clinical-mdr-api/common/utils.py @@ -19,20 +19,18 @@ class VisitClass(Enum): - SINGLE_VISIT = "Single visit" - SPECIAL_VISIT = "Special visit" - NON_VISIT = "Non visit" - UNSCHEDULED_VISIT = "Unscheduled visit" - MANUALLY_DEFINED_VISIT = "Manually defined visit" + SINGLE_VISIT = "SINGLE_VISIT" + SPECIAL_VISIT = "SPECIAL_VISIT" + NON_VISIT = "NON_VISIT" + UNSCHEDULED_VISIT = "UNSCHEDULED_VISIT" + MANUALLY_DEFINED_VISIT = "MANUALLY_DEFINED_VISIT" class VisitSubclass(Enum): - SINGLE_VISIT = "Single visit" - ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV = ( - "Additional subvisit in a group of subvisits" - ) - ANCHOR_VISIT_IN_GROUP_OF_SUBV = "Anchor visit in group of subvisits" - REPEATING_VISIT = "Repeating visit" + SINGLE_VISIT = "SINGLE_VISIT" + ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV = "ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV" + ANCHOR_VISIT_IN_GROUP_OF_SUBV = "ANCHOR_VISIT_IN_GROUP_OF_SUBV" + REPEATING_VISIT = "REPEATING_VISIT" @dataclass @@ -336,19 +334,25 @@ def booltostr(value: bool | str | int | None, true_format: str = "Yes") -> str: @overload def convert_to_datetime(value: neo4j.time.DateTime) -> datetime: ... @overload +def convert_to_datetime(value: datetime) -> datetime: ... +@overload def convert_to_datetime(value: None) -> None: ... -def convert_to_datetime(value: neo4j.time.DateTime | None) -> datetime | None: +def convert_to_datetime( + value: neo4j.time.DateTime | datetime | None, +) -> datetime | None: """ Converts a neo4j.time.DateTime object from the database to a Python datetime object. Args: - value (neo4j.time.DateTime | None): The DateTime object to convert or None. + value (neo4j.time.DateTime | datetime | None): The object to convert or None. Returns: datetime.datetime: The Python datetime object or None if `value` is None. """ if value is None: return None + if isinstance(value, datetime): + return value if not isinstance(value, neo4j.time.DateTime): raise TypeError(f"Expected neo4j.time.DateTime, got {type(value)}") return value.to_native() @@ -365,8 +369,8 @@ def validate_max_skip_clause(page_number: int, page_size: int) -> None: # neo4j supports `SKIP {val}` values which fall within unsigned 64-bit integer range ValidationException.raise_if( - max(1, page_number) * max(1, page_size) > settings.max_int_neo4j, - msg=f"(page_number * page_size) value cannot be bigger than {settings.max_int_neo4j}", + max(1, page_number) * max(1, page_size) >= settings.max_int_neo4j, + msg=f"(page_number * page_size) value must be smaller than {settings.max_int_neo4j}", ) diff --git a/clinical-mdr-api/compose.dev.yaml b/clinical-mdr-api/compose.dev.yaml index 47e8bd37..5f8a23ac 100644 --- a/clinical-mdr-api/compose.dev.yaml +++ b/clinical-mdr-api/compose.dev.yaml @@ -42,6 +42,25 @@ services: source: ./ target: /app + extensions-api: + extends: + file: compose.yaml + service: extensions-api + build: + args: + TARGET: ${BUILD_TARGET:-dev} + UID: ${UID:-1000} + environment: + NEO4J_DSN: "${NEO4J_DSN:-bolt://neo4j:changeme1234@database:7687/mdrdb}" + UVICORN_ROOT_PATH: "" + UVICORN_RELOAD: "true" + ports: + - "127.0.0.1:5679:5678" + volumes: + - type: bind + source: ./ + target: /app + database: extends: file: compose.yaml diff --git a/clinical-mdr-api/compose.yaml b/clinical-mdr-api/compose.yaml index f6ae4c9d..64201517 100644 --- a/clinical-mdr-api/compose.yaml +++ b/clinical-mdr-api/compose.yaml @@ -73,6 +73,24 @@ services: ports: - "${BIND_ADDRESS:-127.0.0.1}:${CONSUMER_API_PORT:-8008}:8000" + # Extensions API service + extensions-api: + image: ${API_IMAGE:-clinical-mdr-api} + environment: + NEO4J_DSN: "${NEO4J_DSN:-bolt://neo4j:changeme1234@database:7687/neo4j}" + ALLOW_ORIGIN_REGEX: "${ALLOW_ORIGIN_REGEX:-.*}" + OAUTH_ENABLED: "${OAUTH_ENABLED:-False}" + OAUTH_RBAC_ENABLED: "${OAUTH_RBAC_ENABLED:-False}" + OAUTH_METADATA_URL: "${OAUTH_METADATA_URL:-}" + OAUTH_API_APP_ID: "${OAUTH_API_APP_ID:-}" + OAUTH_API_APP_SECRET: "${OAUTH_API_APP_SECRET:-}" + OAUTH_SWAGGER_APP_ID: "${OAUTH_SWAGGER_APP_ID:-}" + UVICORN_APP: "extensions.extensions_api:app" + UVICORN_LOG_CONFIG: "${UVICORN_LOG_CONFIG:-}" + PRODEX_API_MOCK: "true" + ports: + - "${BIND_ADDRESS:-127.0.0.1}:${EXTENSIONS_API_PORT:-8009}:8000" + # OpenStudyBuilder API service for running tests in pipeline dev: extends: @@ -100,6 +118,15 @@ services: - type: bind source: ./consumer_api/apiVersion target: /app/consumer_api/apiVersion + - type: bind + source: ./extensions/reports + target: /app/extensions/reports + - type: bind + source: ./extensions/openapi.json + target: /app/extensions/openapi.json + - type: bind + source: ./extensions/apiVersion + target: /app/extensions/apiVersion allure: working_dir: /app diff --git a/clinical-mdr-api/consumer_api/apiVersion b/clinical-mdr-api/consumer_api/apiVersion index 4b9b35d8..c21e67e6 100644 --- a/clinical-mdr-api/consumer_api/apiVersion +++ b/clinical-mdr-api/consumer_api/apiVersion @@ -1 +1 @@ -0.1.112 +0.1.113 diff --git a/clinical-mdr-api/consumer_api/openapi.json b/clinical-mdr-api/consumer_api/openapi.json index 40ef03b6..0e4f30b3 100644 --- a/clinical-mdr-api/consumer_api/openapi.json +++ b/clinical-mdr-api/consumer_api/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "OpenStudyBuilder Consumer API", "description": "\n## NOTICE\n\nThis license information is applicable to the swagger documentation of the clinical-mdr-api, that is the openapi.json.\n\n## License Terms (MIT)\n\nCopyright (C) 2025 Novo Nordisk A/S, Danish company registration no. 24256790\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n## Licenses and Acknowledgements for Incorporated Software\n\nThis component contains software licensed under different licenses when compiled, please refer to the third-party-licenses.md file for further information and full license texts.\n\n## Authentication\n\nSupports OAuth2 [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1),\nat paths described in the OpenID Connect Discovery metadata document (whose URL is defined by the `OAUTH_METADATA_URL` environment variable).\n\nMicrosoft Identity Platform documentation can be read \n([here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow)).\n", - "version": "0.1.112" + "version": "0.1.113" }, "paths": { "/": { @@ -3816,21 +3816,21 @@ "VisitClass": { "type": "string", "enum": [ - "Single visit", - "Special visit", - "Non visit", - "Unscheduled visit", - "Manually defined visit" + "SINGLE_VISIT", + "SPECIAL_VISIT", + "NON_VISIT", + "UNSCHEDULED_VISIT", + "MANUALLY_DEFINED_VISIT" ], "title": "VisitClass" }, "VisitSubclass": { "type": "string", "enum": [ - "Single visit", - "Additional subvisit in a group of subvisits", - "Anchor visit in group of subvisits", - "Repeating visit" + "SINGLE_VISIT", + "ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV", + "ANCHOR_VISIT_IN_GROUP_OF_SUBV", + "REPEATING_VISIT" ], "title": "VisitSubclass" } diff --git a/clinical-mdr-api/extensions/README.md b/clinical-mdr-api/extensions/README.md new file mode 100644 index 00000000..9d0ef315 --- /dev/null +++ b/clinical-mdr-api/extensions/README.md @@ -0,0 +1,424 @@ +# OpenStudyBuilder API Extensions + +This folder contains API extensions to the OpenStudyBuilder API. The extensions system allows for modular functionality to be added to the main API without modifying the core codebase. + +## Architecture Overview + +The extensions system provides: +- **Dynamic Extension Loading**: Extensions are automatically discovered and loaded at runtime +- **Isolated API Routers**: Each extension can define its own API endpoints +- **Shared Infrastructure**: Extensions can leverage common utilities, authentication, and database access +- **Independent Testing**: Each extension has its own test suite +- **Separate API Server**: Extensions run on a dedicated FastAPI instance (default port 8009) + +## Directory Structure + +``` +extensions/ +├── README.md # This file +├── extensions_api.py # Main FastAPI application for extensions +├── common.py # Shared utilities for extensions +├── apiVersion # Version file for the extensions API +├── openapi.json # Generated OpenAPI specification +├── reports/ # Test coverage and reports +├── __init__.py # Makes extensions a Python package +│ +├── hello/ # Example "hello" extension +│ ├── __init__.py +│ ├── hello_main.py # Router definition (required) +│ ├── db.py # Database operations (recommended) +│ └── tests/ # Extension tests (recommended) +│ ├── __init__.py +│ └── test_extension.py +│ +├── my_extension/ # "my_extension" extension +│ ├── __init__.py +│ ├── my_extension_main.py # Router definition (required) +│ ├── models.py # Pydantic models (recommended) +│ ├── db.py # Database operations (recommended) +│ ├── db_models.py # Neomodel database models (optional) +│ ├── api_client.py # External API client (optional) +│ ├── mock/ # Mock data for development/testing +│ │ └── *.json +│ └── tests/ # Extension tests (recommended) +│ ├── __init__.py +│ └── test_extension.py +| +├── system/ # System/health check extension +│ ├── __init__.py +│ ├── system_main.py +│ └── tests/ +│ ├── __init__.py +│ └── test_extension.py +│ +└── tests/ # Common and auth tests for extensions + ├── __init__.py + ├── auth/ + │ └── integration/ + │ ├── __init__.py + │ └── routes.py # Extensions API routes and their required access roles (required) + ├── conftest.py # Common pytest fixtures + └── test_common.py + +``` + +## How to run extensions locally + +### Prerequisites +- Python 3.13+ installed +- Pipenv installed +- Neo4j database running (configure `NEO4J_DSN` environment variable) +- Dependencies installed: `pipenv install` + +### Start the Extensions API Server + +Run the development server with auto-reload: + +```bash +pipenv run extensions-api-dev +``` + +This will start the FastAPI application on `http://localhost:8009` + +### Access the API + +Once running, you can access: +- **API Documentation (Swagger)**: http://localhost:8009/docs +- **Alternative API Docs (ReDoc)**: http://localhost:8009/redoc +- **OpenAPI Schema**: http://localhost:8009/openapi.json + +### Environment Variables + +Configure these environment variables in a `.env` file or export them: + +```bash +NEO4J_DSN=bolt://neo4j:password@localhost:7687 +OAUTH_ENABLED=false # Set to true if using authentication +``` + + + + +## How Extensions Are Loaded + +1. The `extensions_api.py` file contains the main FastAPI application +2. The `load_extensions()` function automatically discovers all extensions: + - Scans all subdirectories in the `extensions/` folder + - Looks for files named `{extension_name}_main.py` + - Dynamically imports the router from each extension + - Mounts the router with a prefix based on the extension name (e.g., `/hello`, `/my_extension`) + +## Creating a New Extension + +Follow these steps to create a new extension: + +### 1. Create Extension Directory + +Create a new folder in `extensions/` with your extension name (use lowercase, underscores for multi-word names): + +```bash +mkdir extensions/my_extension +``` + +### 2. Create Required Files + +#### `__init__.py` +Create an empty `__init__.py` file to make it a Python package: + +```bash +touch extensions/my_extension/__init__.py +``` + +#### `my_extension_main.py` (Required) +This is the main file that defines your extension's API router. The filename must follow the pattern `{extension_name}_main.py`: + +```python +from fastapi import APIRouter + +from common.auth import rbac +from common.auth.dependencies import security + +router = APIRouter( + tags=["MyExtension"], +) + + +@router.get( + "/example", + dependencies=[security, rbac.ADMIN_READ], + status_code=200, +) +def get_example(): + """ + Example endpoint that demonstrates the basic structure. + """ + return {"message": "Hello from my extension!"} +``` + +**Key Points:** +- The module **must** export a variable named `router` (an instance of `APIRouter`) +- Use `dependencies=[security, rbac.ADMIN_READ]` for authentication (optional) +- The extension will be accessible at `/my-extension/example` (underscores become hyphens) + +### 3. Optional: Add Database Operations + +If your extension needs database access, create a `db.py` file: + +```python +from neomodel.sync_.core import db + + +def get_data_from_neo4j(): + """Execute Cypher queries against Neo4j database.""" + query = """ + MATCH (n:MyNode) + RETURN count(n) as count + """ + results, _ = db.cypher_query(query) + return results[0][0] if results else 0 +``` + +### 4. Optional: Add Pydantic Models + +Create a `models.py` file for request/response models: + +```python +from pydantic import BaseModel, Field + + +class MyRequest(BaseModel): + name: str = Field(..., description="Name of the resource") + value: int = Field(gt=0, description="Positive integer value") + + +class MyResponse(BaseModel): + id: str + name: str + created_at: str +``` + +### 5. Add Tests + +Create a `tests/` directory with test files: + +```bash +mkdir extensions/my_extension/tests +touch extensions/my_extension/tests/__init__.py +``` + +Create `tests/test_extension.py`: + +```python +import pytest +from fastapi.testclient import TestClient + +from extensions.extensions_api import app + + +@pytest.fixture(scope="module") +def api_client(): + """Create FastAPI test client""" + yield TestClient(app) + + +def test_get_example(api_client): + res = api_client.get("/my-extension/example") + assert res.status_code == 200 + assert res.json() == {"message": "Hello from my extension!"} +``` + +### 6. Run Your Extension + +Start the extensions API server: + +```bash +pipenv run extensions-api-dev +``` + +The extension will be automatically loaded and available at: +- Endpoint: `http://localhost:8009/my-extension/example` +- OpenAPI Docs: `http://localhost:8009/docs` + +### 7. Run Tests + +Run all extension tests: + +```bash +pipenv run extensions-test +``` + +Run schemathesis API tests: + +```bash +pipenv run extensions-schemathesis +``` + +## Available Pipenv Commands + +The following commands are available for working with extensions: + +### `pipenv run extensions-api-dev` +Starts the extensions API development server with auto-reload enabled on port 8009. This command launches a uvicorn server that automatically detects and loads all extensions from the `extensions/` folder. Any code changes will trigger an automatic reload, making it ideal for development. The API will be accessible at `http://localhost:8009` with interactive documentation at `/docs`. + +### `pipenv run extensions-openapi` +Generates the OpenAPI specification file for the extensions API. This command creates or updates the `extensions/openapi.json` file based on all registered endpoints from loaded extensions. The generated OpenAPI schema includes all extension routes, request/response models, authentication requirements, and endpoint documentation. This file is useful for API documentation, client SDK generation, and API testing tools. + +### `pipenv run extensions-lint` +Runs PyLint static code analysis on all extension code. This command checks all Python files in the `extensions/` directory for code quality issues, potential bugs, style violations, and adherence to coding standards. It helps maintain consistent code quality across all extensions and catches common programming errors before they reach production. + +### `pipenv run extensions-test` +Executes the complete test suite for all extensions. This command runs pytest with coverage reporting enabled, testing all extension endpoints and functionality. Tests are run in parallel using pytest-xdist for faster execution, with coverage reports generated in both HTML and XML formats in `extensions/reports/`. The command automatically discovers and runs all test files in each extension's `tests/` directory, excluding authentication tests by default. + +### `pipenv run extensions-testauth` +Executes the authentication test suite for all extensions, with coverage reports generated in both HTML and XML formats in `extensions/reports/`. + +### `pipenv run extensions-schemathesis` +Performs automated API testing using Schemathesis, a property-based testing tool for OpenAPI/Swagger specs. This command generates test cases automatically from the OpenAPI schema and sends them to the running extensions API server. It validates that all endpoints conform to their OpenAPI specifications, checking for proper response codes, data types, and edge cases. The tool runs 100 test examples per endpoint and generates detailed reports in `extensions/reports/`, helping identify API contract violations and potential bugs. + +## Available Utilities + +Extensions can use utilities from `extensions/common.py`: + +### Pagination +```python +from extensions.common import ( + PAGE_NUMBER_QUERY, + PAGE_SIZE_QUERY, + PaginatedResponse, +) + +@router.get("/items") +def get_items( + page_size: PAGE_SIZE_QUERY = 10, + page_number: PAGE_NUMBER_QUERY = 1, +): + items = fetch_items() + return PaginatedResponse.from_input( + request=request, + items=items, + total=len(items), + page_size=page_size, + page_number=page_number, + ) +``` + +### Logging +```python +from extensions.common import Logger + +log = Logger(__name__) +log.info("Extension loaded successfully") +``` + +### API Version +```python +from extensions.common import get_api_version + +version = get_api_version() +``` + +## Authentication & Authorization + +Extensions inherit the authentication system from the main API: + +```python +from common.auth import rbac +from common.auth.dependencies import security + +# Require authentication +@router.get("/protected", dependencies=[security]) +def protected_endpoint(): + return {"message": "You are authenticated!"} + +# Require specific role +@router.post("/admin-only", dependencies=[security, rbac.ADMIN_WRITE]) +def admin_endpoint(): + return {"message": "Admin access granted!"} +``` + +Available RBAC permissions: +- `rbac.ADMIN_READ` - Admin read access +- `rbac.ADMIN_WRITE` - Admin write access +- Check `common/auth/rbac.py` for more options + +## Best Practices + +1. **Naming Convention**: Use lowercase with underscores for folder names (e.g., `my_extension`) +2. **Main Module**: Always name your main file `{extension_name}_main.py` +3. **Router Export**: Always export a `router` variable from your main module +4. **Tags**: Use meaningful tags for your router to organize OpenAPI documentation +5. **Documentation**: Add docstrings to all endpoints for auto-generated API docs +6. **Testing**: Write comprehensive tests for all endpoints +7. **Error Handling**: Use exceptions from `common.exceptions` for consistent error responses +8. **Models**: Define Pydantic models for request/response validation +9. **Database**: Keep database logic separate in a `db.py` module +10. **Dependencies**: Reuse existing services from `clinical_mdr_api.services` when possible + +## Common Patterns + +### External API Integration +```python +import requests +from common.config import settings + + +class ExternalApiClient: + def __init__(self): + self.base_url = settings.external_api_url + self.api_key = settings.external_api_key + + def fetch_data(self, param: str): + response = requests.get( + f"{self.base_url}/data", + params={"param": param}, + headers={"Authorization": f"Bearer {self.api_key}"} + ) + response.raise_for_status() + return response.json() +``` + +### Database Models with Neomodel +```python +from neomodel import ( + StructuredNode, + StringProperty, + IntegerProperty, + RelationshipTo, +) + + +class MyNode(StructuredNode): + name = StringProperty(unique_index=True, required=True) + value = IntegerProperty() + + # Relationships + related_to = RelationshipTo('OtherNode', 'RELATED_TO') +``` + +### Error Handling +```python +from common.exceptions import ( + BusinessLogicException, + NotFoundException, + ValidationException, +) + +@router.get("/item/{item_id}") +def get_item(item_id: str): + item = find_item(item_id) + if not item: + raise NotFoundException(f"Item with id {item_id} not found") + return item +``` + + + +## Examples + +Refer to existing extensions for working examples: +- **hello**: Simple extension showing basic structure +- **system**: Health check and system monitoring endpoints + + + + diff --git a/clinical-mdr-api/extensions/apiVersion b/clinical-mdr-api/extensions/apiVersion new file mode 100644 index 00000000..bcab45af --- /dev/null +++ b/clinical-mdr-api/extensions/apiVersion @@ -0,0 +1 @@ +0.0.3 diff --git a/clinical-mdr-api/extensions/common.py b/clinical-mdr-api/extensions/common.py new file mode 100644 index 00000000..00d4be32 --- /dev/null +++ b/clinical-mdr-api/extensions/common.py @@ -0,0 +1,201 @@ +import logging +import os +import urllib.parse +from datetime import datetime, timezone +from enum import Enum +from typing import Annotated, Any, Generic, Self, TypeVar + +from fastapi import Query, Request +from neomodel.sync_.core import db +from pydantic import BaseModel, Field +from requests.utils import requote_uri + +from common.config import settings +from common.exceptions import ValidationException +from common.utils import filter_sort_valid_keys_re, get_db_result_as_dict + +T = TypeVar("T") + + +class SortByType(Enum): + STRING = "string" + NUMBER = "number" + + +class PaginatedResponse(BaseModel, Generic[T]): + """ + Paginated response model + """ + + self: Annotated[ + str, Field(description="Pagination link pointing to the current page") + ] + prev: Annotated[ + str, Field(description="Pagination link pointing to the previous page") + ] + next: Annotated[str, Field(description="Pagination link pointing to the next page")] + total: Annotated[int, Field(description="Total number of items")] = 0 + items: Annotated[list[T], Field(description="List of items")] + + @classmethod + def from_input( + cls, + request: Request, + sort_by: str, + sort_order: str, + page_size: int, + page_number: int, + items: list[T], + query_param_names: list[str] | None = None, + total: int = 0, + ) -> Self: + path = request.url.path + + # Extract query parameters not related to sorting/pagination from the request + query_params = "" + if query_param_names: + for query_param_name in query_param_names: + query_param_val = request.query_params.get(query_param_name) + if query_param_val: + query_params = ( + f"{query_params}{query_param_name}={query_param_val}&" + ) + query_params = requote_uri(query_params) + + prev_page_number = page_number - 1 if page_number > 1 else 1 + + self_link = f"{path}?{query_params}sort_by={sort_by}&sort_order={sort_order}&page_size={page_size}&page_number={page_number}" + prev_link = f"{path}?{query_params}sort_by={sort_by}&sort_order={sort_order}&page_size={page_size}&page_number={prev_page_number}" + next_link = f"{path}?{query_params}sort_by={sort_by}&sort_order={sort_order}&page_size={page_size}&page_number={page_number + 1}" + + # pylint: disable=kwarg-superseded-by-positional-arg + return cls( + self=urlencode_link(self_link), + prev=urlencode_link(prev_link), + next=urlencode_link(next_link), + items=items, + total=total, + ) + + +def query( + cypher_query, + params: dict[Any, Any] | None = None, + handle_unique: bool = True, + retry_on_session_expire: bool = False, + resolve_objects: bool = False, + to_dict_list: bool = True, +): + """ + Wraps `db.cypher_query()` + + Returns: + list[dict] | tuple: If `to_dict_list` is True, returns a list of dictionaries representing the query results. + If `to_dict_list` is False, returns a tuple containing the rows and columns from the query. + """ + if params is None: + params = {} + + try: + rows, columns = db.cypher_query( + query=cypher_query, + params=params, + handle_unique=handle_unique, + retry_on_session_expire=retry_on_session_expire, + resolve_objects=resolve_objects, + ) + except Exception as e: + raise ValidationException(msg=f"Database query failed: {e.message}") from e + + if to_dict_list: + return [get_db_result_as_dict(row, columns) for row in rows] + + return rows, columns + + +def urlencode_link(link: str) -> str: + """URL encodes a link""" + + url = urllib.parse.urlparse(link) + query_params = urllib.parse.parse_qs(url.query, keep_blank_values=True) + + url = url._replace(query=urllib.parse.urlencode(query_params, True)) + return urllib.parse.urlunparse(url) + + +def db_pagination_clause(page_size: int, page_number: int) -> str: + # Ensure Cypher injection would not be possible even if values weren't integer types + if not isinstance(page_size, int) or not isinstance(page_number, int): + raise TypeError("Expected page_size and page_number to be integers") + + limit = f"LIMIT {page_size}" if page_size > 0 else "" + return f"SKIP {page_number - 1} * {page_size} {limit}" + + +def db_sort_clause( + sort_by: str, + sort_order: str = "ASC", + sort_by_type: SortByType = SortByType.STRING, + secondary_sort_fields: str = "uid", +) -> str: + # Ensure Cypher injection would not be exploitable even if sort_by keys were not checked + if not filter_sort_valid_keys_re.fullmatch(sort_by): + raise ValidationException(msg=f"Invalid sorting key: {sort_by}") + + if sort_by_type == SortByType.NUMBER: + primary_sort = f"toFloat({sort_by}) {sort_order}" + else: + primary_sort = f"toLower(toString({sort_by})) {sort_order}" + + # Add hash of all relevant properties as secondary sort for consistent ordering + if secondary_sort_fields: + secondary_sort = f"apoc.util.md5([{secondary_sort_fields}]) ASC" + return f"ORDER BY {primary_sort}, {secondary_sort}" + + return f"ORDER BY {primary_sort}" + + +def get_api_version() -> str: + version_path = os.path.join("./extensions", "apiVersion") + with open(version_path, "r", encoding="utf-8") as file: + return file.read().strip() + + +# Reusable Query parameter for page_number with maximum constraint +PAGE_NUMBER_QUERY = Query( + ge=0, + le=settings.max_page_number, +) + +# Reusable Query parameter for page_size with maximum constraint (allows page_size=0 for "all rows") +PAGE_SIZE_QUERY = Query( + ge=0, + le=settings.max_page_size, +) + + +class Logger: + + def __init__(self, name: str = __name__): + self.full_log: list[str] = [] + self.log = logging.getLogger(name) + + @classmethod + def format_message(cls, msg: str, level: str = "INFO") -> str: + return f"[{datetime.now(timezone.utc)}] {level} {msg}" + + def debug(self, msg: str) -> None: + self.log.debug(msg) + self.full_log.append(self.format_message(msg, "DEBUG")) + + def info(self, msg: str) -> None: + self.log.info(msg) + self.full_log.append(self.format_message(msg, "INFO")) + + def warning(self, msg: str) -> None: + self.log.warning(msg) + self.full_log.append(self.format_message(msg, "WARNING")) + + def error(self, msg: str) -> None: + self.log.error(msg) + self.full_log.append(self.format_message(msg, "ERROR")) diff --git a/clinical-mdr-api/extensions/extensions_api.py b/clinical-mdr-api/extensions/extensions_api.py new file mode 100644 index 00000000..0f9e6f11 --- /dev/null +++ b/clinical-mdr-api/extensions/extensions_api.py @@ -0,0 +1,369 @@ +"""RESTful API endpoints that extend the main API functionality""" + +# Placed at the top to ensure logging is configured before anything else is loaded +import importlib +import sys +from typing import Any + +from fastapi.exceptions import RequestValidationError +from opencensus.trace.print_exporter import PrintExporter + +from common.logger import default_logging_config, log_exception +from common.telemetry.request_metrics import patch_neomodel_database +from common.telemetry.tracing_middleware import TracingMiddleware +from extensions.common import get_api_version + +default_logging_config() + +# pylint: disable=wrong-import-position,wrong-import-order,ungrouped-imports +import logging +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI, Request, status +from fastapi.encoders import jsonable_encoder +from fastapi.middleware import Middleware +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.openapi.utils import get_openapi +from fastapi.responses import JSONResponse +from fastapi.routing import APIRoute +from neomodel import config as neomodel_config +from opencensus.ext.azure.trace_exporter import AzureExporter +from opencensus.trace.samplers import AlwaysOnSampler +from pydantic import ValidationError +from starlette_context.middleware import RawContextMiddleware + +from common.auth.dependencies import security +from common.auth.discovery import reconfigure_with_openid_discovery +from common.config import settings +from common.exceptions import MDRApiBaseException +from common.models.error import ErrorResponse +from common.telemetry.traceback_middleware import ExceptionTracebackMiddleware + +log = logging.getLogger(__name__) + +# Configure Neo4J connection on startup +neo4j_dsn = os.getenv("NEO4J_DSN") +if neo4j_dsn: + neomodel_config.DATABASE_URL = neo4j_dsn + log.info("Neo4j DSN set to: %s", neo4j_dsn.split("@")[-1]) + + +# Middlewares - please don't use app.add_middleware() as that inserts them to the beginning of the list +middlewares = [] + +# gzip compress responses +if settings.gzip_response_min_size: + middlewares.append( + Middleware( + GZipMiddleware, + minimum_size=settings.gzip_response_min_size, + compresslevel=settings.gzip_level, + ) + ) + +# Context middleware - must come before TracingMiddleware +middlewares.append(Middleware(RawContextMiddleware)) + +# Tracing middleware +if settings.tracing_enabled: + + # Azure Application Insights integration for tracing + if settings.appinsights_connection: + tracing_exporter = AzureExporter( + connection_string=settings.appinsights_connection, + enable_local_storage=False, + ) + + elif settings.zipkin_host: + # opencensus-ext-zipkin is a dev-only package dependency + from opencensus.ext.zipkin.trace_exporter import ZipkinExporter + + tracing_exporter = ZipkinExporter( + service_name="extension-api", + host_name=settings.zipkin_host, + port=settings.zipkin_port, + endpoint=settings.zipkin_endpoint, + protocol=settings.zipkin_protocol, + ) + + else: + tracing_exporter = PrintExporter() + + middlewares.append( + Middleware( + TracingMiddleware, + sampler=AlwaysOnSampler(), + exporter=tracing_exporter, + exclude_paths={"*/system/healthcheck"}, + exclude_clients={"127.0.0.1", "::1"}, + ) + ) + + patch_neomodel_database() + +middlewares.append( + Middleware( + CORSMiddleware, + allow_origin_regex=settings.allow_origin_regex, + allow_credentials=settings.allow_credentials, + allow_methods=settings.allow_methods, + allow_headers=settings.allow_headers, + expose_headers=["traceresponse"], + ) +) + +# Convert all uncaught exceptions to response before returning to TracingMiddleware +middlewares.append(Middleware(ExceptionTracebackMiddleware)) + + +@asynccontextmanager +async def lifespan(_app: FastAPI): + if settings.oauth_enabled: + # Reconfiguring Swagger UI settings with OpenID Connect discovery + await reconfigure_with_openid_discovery() + yield + + +app = FastAPI( + title="OpenStudyBuilder API Extensions", + version=get_api_version(), + middleware=middlewares, + lifespan=lifespan, + swagger_ui_init_oauth=settings.swagger_ui_init_oauth, + swagger_ui_parameters={"docExpansion": "none"}, + description=""" +## NOTICE + +This license information is applicable to the swagger documentation of the clinical-mdr-api, that is the openapi.json. + +## License Terms (MIT) + +Copyright (C) 2025 Novo Nordisk A/S, Danish company registration no. 24256790 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Licenses and Acknowledgements for Incorporated Software + +This component contains software licensed under different licenses when compiled, please refer to the third-party-licenses.md file for further information and full license texts. + +## Authentication + +Supports OAuth2 [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1), +at paths described in the OpenID Connect Discovery metadata document (whose URL is defined by the `OAUTH_METADATA_URL` environment variable). + +Microsoft Identity Platform documentation can be read +([here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow)). +""", +) + +app.openapi_version = "3.1.0" + + +@app.exception_handler(MDRApiBaseException) +async def extension_api_exception_handler( + request: Request, exception: MDRApiBaseException +): + """Returns an HTTP error code associated to given exception.""" + + await log_exception(request, exception) + + ExceptionTracebackMiddleware.add_traceback_attributes(exception) + + return JSONResponse( + status_code=exception.status_code, + content=jsonable_encoder(ErrorResponse(request, exception)), + headers=exception.headers, + ) + + +@app.exception_handler(ValidationError) +async def pydantic_validation_error_handler( + request: Request, exception: ValidationError +): + """Returns `400 Bad Request` http error status code in case Pydantic detects validation issues + with supplied payloads or parameters.""" + + await log_exception(request, exception) + + ExceptionTracebackMiddleware.add_traceback_attributes(exception) + + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=jsonable_encoder(ErrorResponse(request, exception)), + ) + + +@app.exception_handler(RequestValidationError) +async def handle_request_validation_error( + request: Request, exception: RequestValidationError +) -> JSONResponse: + await log_exception(request, exception) + + ExceptionTracebackMiddleware.add_traceback_attributes(exception) + + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=jsonable_encoder(ErrorResponse(request, exception)), + headers={}, + ) + + +@app.exception_handler(ValueError) +async def value_error_handler(request: Request, exception: ValueError): + """Returns `400 Bad Request` http error status code in case ValueError is raised""" + + await log_exception(request, exception) + + ExceptionTracebackMiddleware.add_traceback_attributes(exception) + + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=jsonable_encoder(ErrorResponse(request, exception)), + ) + + +class PathContext: + + def __init__(self, path, orig_path=None): + self.path = path + self.orig_path = orig_path + + def __enter__(self): + self.orig_path = list(sys.path) + if self.path is not None: + sys.path = self.path + sys.path + + def __exit__(self, etype, evalue, etraceback): + sys.path = self.orig_path + + +def load_extensions(): + """Function to load extensions when this module is imported.""" + # Loop through all subfolders of the 'extensions' folder dynamically + # and load their '{extension}_main.py' API router files if they exist + extensions_folder = os.path.dirname(__file__) + for extension_name in os.listdir(extensions_folder): + extension_path = os.path.join(extensions_folder, extension_name) + extension_main_module = f"{extension_name}_main" + extension_main_file = os.path.join( + extension_path, extension_main_module + ".py" + ) + if os.path.isdir(extension_path) and os.path.exists(extension_main_file): + log.info( + "Loading extension '%s' from '%s'", + extension_main_module, + extension_path, + ) + with PathContext([extension_path]): + try: + module = importlib.import_module(extension_main_module) + router = module.router + except Exception as e: + log.error( + "Error loading extension '%s' from '%s'", + extension_main_module, + extension_path, + ) + log.debug("Details:", exc_info=e) + raise + app.include_router( + router, + prefix="/" + extension_name.replace("_", "-"), + ) + + +def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + + openapi_schema = get_openapi( + title=app.title, + version=app.version, + openapi_version=app.openapi_version, + description=app.description, + routes=app.routes, + ) + + openapi_schema["servers"] = [{"url": settings.openapi_schema_api_root_path}] + + if settings.oauth_enabled: + if "components" not in openapi_schema: + openapi_schema["components"] = {} + + if "securitySchemes" not in openapi_schema["components"]: + openapi_schema["components"]["securitySchemes"] = {} + + # Add 'BearerJwtAuth' security schema globally + openapi_schema["components"]["securitySchemes"]["BearerJwtAuth"] = { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", # optional, arbitrary value for documentation purposes + "in": "header", + "name": "Authorization", + "description": "Access token that will be sent as `Authorization: Bearer {token}` header in all requests", + } + + openapi_schema["components"]["securitySchemes"][ + "OAuth2AuthorizationCodeBearer" + ]["flows"]["authorizationCode"]["scopes"] = { + "api:///API.call": "Make calls to the API" + } + + # Add 'BearerJwtAuth' security method to all endpoints + api_router = [route for route in app.routes if isinstance(route, APIRoute)] + for route in api_router: + if not any( + dependency + for dependency in route.dependencies + if dependency == security + ): + continue + path = getattr(route, "path") + methods = [method.lower() for method in getattr(route, "methods")] + + for method in methods: + endpoint_security: list[Any] = openapi_schema["paths"][path][ + method + ].get("security", []) + endpoint_security.append({"BearerJwtAuth": []}) + openapi_schema["paths"][path][method]["security"] = endpoint_security + + # Add `400 Bad Request` error response to all endpoints + for path, path_item in openapi_schema["paths"].items(): + for method, operation in path_item.items(): + if "responses" not in operation: + operation["responses"] = {} + if "400" not in operation["responses"]: + operation["responses"]["400"] = { + "description": "Bad Request", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ErrorResponse"} + } + }, + } + + app.openapi_schema = openapi_schema + return app.openapi_schema + + +load_extensions() + +setattr(app, "openapi", custom_openapi) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "extensions.extensions_api:app", + host=os.getenv("UVICORN_HOST", "127.0.0.1"), + port=int(os.getenv("UVICORN_PORT", "8009")), + reload=True, + ) diff --git a/clinical-mdr-api/extensions/hello/__init__.py b/clinical-mdr-api/extensions/hello/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clinical-mdr-api/extensions/hello/db.py b/clinical-mdr-api/extensions/hello/db.py new file mode 100644 index 00000000..cf967bfd --- /dev/null +++ b/clinical-mdr-api/extensions/hello/db.py @@ -0,0 +1,14 @@ +# pylint: disable=invalid-name +# pylint: disable=redefined-builtin + +from extensions.common import query + + +def get_node_count() -> int: + result = query( + """ + MATCH (n) + RETURN count(n) AS node_count + """ + ) + return result[0]["node_count"] if result else 0 diff --git a/clinical-mdr-api/extensions/hello/hello_main.py b/clinical-mdr-api/extensions/hello/hello_main.py new file mode 100644 index 00000000..7d6d0a6c --- /dev/null +++ b/clinical-mdr-api/extensions/hello/hello_main.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter + +from common.auth import rbac +from common.auth.dependencies import security +from extensions.hello import db as DB + +router = APIRouter( + tags=["Hello"], +) + + +# GET endpoint that returns the count of nodes in the database +@router.get( + "/nodes-count", + dependencies=[security, rbac.ADMIN_READ], + status_code=200, +) +def get_nodes_count() -> int: + """ + Returns the count of nodes in the database. + """ + count = DB.get_node_count() + return count diff --git a/clinical-mdr-api/extensions/hello/tests/__init__.py b/clinical-mdr-api/extensions/hello/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clinical-mdr-api/extensions/hello/tests/test_extension.py b/clinical-mdr-api/extensions/hello/tests/test_extension.py new file mode 100644 index 00000000..b58cb1cf --- /dev/null +++ b/clinical-mdr-api/extensions/hello/tests/test_extension.py @@ -0,0 +1,18 @@ +# pylint: disable=unused-argument +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +import pytest +from fastapi.testclient import TestClient + +from extensions.extensions_api import app + + +@pytest.fixture(scope="module") +def api_client(): + """Create FastAPI test client""" + yield TestClient(app) + + +def test_get_nodes_count(api_client): + res = api_client.get("/hello/nodes-count") + assert res.status_code == 200 diff --git a/clinical-mdr-api/extensions/system/__init__.py b/clinical-mdr-api/extensions/system/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clinical-mdr-api/extensions/system/system_main.py b/clinical-mdr-api/extensions/system/system_main.py new file mode 100644 index 00000000..bd84c4bd --- /dev/null +++ b/clinical-mdr-api/extensions/system/system_main.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter + +router = APIRouter( + tags=["System"], +) + + +@router.get( + "/healthcheck", + summary="Returns 200 OK status if the system is ready to serve requests", + status_code=200, +) +async def healthcheck() -> str: + return "OK" diff --git a/clinical-mdr-api/extensions/system/tests/__init__.py b/clinical-mdr-api/extensions/system/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/clinical-mdr-api/extensions/system/tests/test_extension.py b/clinical-mdr-api/extensions/system/tests/test_extension.py new file mode 100644 index 00000000..a817460e --- /dev/null +++ b/clinical-mdr-api/extensions/system/tests/test_extension.py @@ -0,0 +1,19 @@ +# pylint: disable=unused-argument +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +import pytest +from fastapi.testclient import TestClient + +from extensions.extensions_api import app + + +@pytest.fixture(scope="module") +def api_client(): + """Create FastAPI test client""" + yield TestClient(app) + + +def test_healthcheck_endpoint(api_client): + res = api_client.get("/system/healthcheck") + assert res.text == '"OK"' + assert res.status_code == 200 diff --git a/clinical-mdr-api/generate_openapi.py b/clinical-mdr-api/generate_openapi.py index ccc7d42c..e27b19ea 100644 --- a/clinical-mdr-api/generate_openapi.py +++ b/clinical-mdr-api/generate_openapi.py @@ -9,7 +9,7 @@ increment_api_version_if_needed, increment_version_number, ) -from consumer_api.consumer_api import custom_openapi +# from consumer_api.consumer_api import custom_openapi log = logging.getLogger(__name__) @@ -26,7 +26,11 @@ def generate_openapi(app_import_path, schema_path, version_path, stdout: bool = version_path = os.path.join("./", version_path) # Generate OpenAPI schema file, increment version number if needed - api_spec_new = custom_openapi() + custom_openapi_func = getattr(module, "custom_openapi", None) + if custom_openapi_func: + api_spec_new = custom_openapi_func() + else: + api_spec_new = app.openapi() try: with open(schema_path, "r", encoding="utf-8") as f: api_spec_old = json.load(f) diff --git a/clinical-mdr-api/openapi.json b/clinical-mdr-api/openapi.json index 1dbe3c12..3a67f84b 100644 --- a/clinical-mdr-api/openapi.json +++ b/clinical-mdr-api/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "OpenStudyBuilder API", "description": "\n## NOTICE\n\nThis license information is applicable to the swagger documentation of the clinical-mdr-api, that is the openapi.json.\n\n## License Terms (MIT)\n\nCopyright (C) 2025 Novo Nordisk A/S, Danish company registration no. 24256790\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n## Licenses and Acknowledgements for Incorporated Software\n\nThis component contains software licensed under different licenses when compiled, please refer to the third-party-licenses.md file for further information and full license texts.\n\n## Authentication\n\nSupports OAuth2 [Authorization Code Flow](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1),\nat paths described in the OpenID Connect Discovery metadata document (whose URL is defined by the `OAUTH_METADATA_URL` environment variable).\n\nMicrosoft Identity Platform documentation can be read \n([here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow)).\n\nAuthentication can be turned off with `OAUTH_ENABLED=false` environment variable. \n\nWhen authentication is turned on, all requests to protected API endpoints must provide a valid bearer (JWT) token inside the `Authorization` http header. \n", - "version": "3.0.569" + "version": "3.0.595" }, "paths": { "/": { @@ -903,6 +903,50 @@ } } }, + "/iso/639": { + "get": { + "tags": [ + "ISO Standards" + ], + "summary": "Get ISO 639 Languages", + "description": "Get a list of ISO 639 languages with their codes.\n\nThe list includes the language name, ISO 639-1 code and ISO 639-2T code.", + "operationId": "get_iso_languages_iso_639_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/ISOLanguageModel" + }, + "type": "array", + "title": "Response Get Iso Languages Iso 639 Get" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + }, + { + "BearerJwtAuth": [] + } + ] + } + }, "/concepts/odms/study-events": { "get": { "tags": [ @@ -1034,11 +1078,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -2254,11 +2298,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -3407,382 +3451,6 @@ } } }, - "/concepts/odms/forms/{odm_form_uid}/vendor-elements": { - "post": { - "tags": [ - "ODM Forms" - ], - "summary": "Adds ODM Vendor Elements to the ODM Form.", - "operationId": "add_vendor_elements_to_odm_form_concepts_odms_forms__odm_form_uid__vendor_elements_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_form_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Form.", - "title": "Odm Form Uid" - }, - "description": "The unique id of the ODM Form." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, - "responses": { - "201": { - "description": "Created - The ODM Vendor Elements were successfully added to the ODM Form.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmForm" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Vendor Elements with the specified 'odm_form_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/forms/{odm_form_uid}/vendor-attributes": { - "post": { - "tags": [ - "ODM Forms" - ], - "summary": "Adds ODM Vendor Attributes to the ODM Form.", - "operationId": "add_vendor_attributes_to_odm_form_concepts_odms_forms__odm_form_uid__vendor_attributes_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_form_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Form.", - "title": "Odm Form Uid" - }, - "description": "The unique id of the ODM Form." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Attribute relationships will\n be replaced with the provided ODM Vendor Attribute relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Attribute relationships will\n be replaced with the provided ODM Vendor Attribute relationships." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, - "responses": { - "201": { - "description": "Created - The ODM Vendor Attributes were successfully added to the ODM Form.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmForm" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Vendor Attributes with the specified 'odm_form_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/forms/{odm_form_uid}/vendor-element-attributes": { - "post": { - "tags": [ - "ODM Forms" - ], - "summary": "Adds ODM Vendor Element attributes to the ODM Form.", - "operationId": "add_vendor_element_attributes_to_odm_form_concepts_odms_forms__odm_form_uid__vendor_element_attributes_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_form_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Form.", - "title": "Odm Form Uid" - }, - "description": "The unique id of the ODM Form." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Element attribute relationships\n will be replaced with the provided ODM Vendor Element attribute relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Element attribute relationships\n will be replaced with the provided ODM Vendor Element attribute relationships." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, - "responses": { - "201": { - "description": "Created - The ODM Vendor Element attributes were successfully added to the ODM Form.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmForm" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Vendor Element attributes with the specified 'odm_form_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/forms/{odm_form_uid}/vendors": { - "post": { - "tags": [ - "ODM Forms" - ], - "summary": "Manages all ODM Vendors by replacing existing ODM Vendors by provided ODM Vendors.", - "operationId": "manage_vendors_of_odm_form_concepts_odms_forms__odm_form_uid__vendors_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_form_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Form.", - "title": "Odm Form Uid" - }, - "description": "The unique id of the ODM Form." - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmVendorsPostInput" - } - } - } - }, - "responses": { - "201": { - "description": "Created - The ODM Vendors were successfully added to the ODM Form.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmForm" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Vendors with the specified 'odm_form_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, "/concepts/odms/item-groups": { "get": { "tags": [ @@ -3914,11 +3582,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -4399,392 +4067,18 @@ "description": "The unique id of the ODM Item Group." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmItemGroupPatchInput" - } - } - } - }, - "responses": { - "200": { - "description": "OK.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmItemGroup" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in draft status.\n- The ODM Item Group had been in 'Final' status before.\n- The library doesn't allow to edit draft versions.\n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - }, - "delete": { - "tags": [ - "ODM Item Groups" - ], - "summary": "Delete draft version of ODM Item Group", - "operationId": "delete_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__delete", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_group_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item Group.", - "title": "Odm Item Group Uid" - }, - "description": "The unique id of the ODM Item Group." - } - ], - "responses": { - "204": { - "description": "No Content - The ODM Item Group was successfully deleted." - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in draft status.\n- The ODM Item Group was already in final state or is in use.\n- The library doesn't allow to delete ODM Item Group.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - An ODM Item Group with the specified 'odm_item_group_uid' could not be found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/item-groups/{odm_item_group_uid}/relationships": { - "get": { - "tags": [ - "ODM Item Groups" - ], - "summary": "Get UIDs of a specific ODM Item Group's relationships", - "operationId": "get_active_relationships_concepts_odms_item_groups__odm_item_group_uid__relationships_get", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_group_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item Group.", - "title": "Odm Item Group Uid" - }, - "description": "The unique id of the ODM Item Group." - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": "Response Get Active Relationships Concepts Odms Item Groups Odm Item Group Uid Relationships Get" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Entity not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/item-groups/{odm_item_group_uid}/versions": { - "get": { - "tags": [ - "ODM Item Groups" - ], - "summary": "List version history for ODM Item Group", - "description": "State before:\n - uid must exist.\n\nBusiness logic:\n - List version history for ODM Item Groups.\n - The returned versions are ordered by start_date descending (newest entries first).\n\nState after:\n - No change\n\nPossible errors:\n - Invalid uid.", - "operationId": "get_odm_item_group_versions_concepts_odms_item_groups__odm_item_group_uid__versions_get", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_group_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item Group.", - "title": "Odm Item Group Uid" - }, - "description": "The unique id of the ODM Item Group." - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmItemGroup" - }, - "title": "Response Get Odm Item Group Versions Concepts Odms Item Groups Odm Item Group Uid Versions Get" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "ODM Item Groups" - ], - "summary": " Create a new version of ODM Item Group", - "description": "State before:\n - uid must exist and the ODM Item Group must be in status Final.\n\nBusiness logic:\n- The ODM Item Group is changed to a draft state.\n\nState after:\n - ODM Item Group changed status to Draft and assigned a new minor version number.\n - Audit trail entry must be made with action of creating a new draft version.\n\nPossible errors:\n - Invalid uid or status not Final.", - "operationId": "create_odm_item_group_version_concepts_odms_item_groups__odm_item_group_uid__versions_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_group_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item Group.", - "title": "Odm Item Group Uid" - }, - "description": "The unique id of the ODM Item Group." - }, - { - "name": "cascade_new_version", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all child elements will also get a new version.", - "default": false, - "title": "Cascade New Version" - }, - "description": "If true, all child elements will also get a new version." - } - ], - "responses": { - "201": { - "description": "OK.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmItemGroup" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The library doesn't allow to create ODM Item Groups.\n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - Reasons include e.g.: \n- The ODM Item Group is not in final status.\n- The ODM Item Group with the specified 'odm_item_group_uid' could not be found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/item-groups/{odm_item_group_uid}/approvals": { - "post": { - "tags": [ - "ODM Item Groups" - ], - "summary": "Approve draft version of ODM Item Group", - "operationId": "approve_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__approvals_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_group_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item Group.", - "title": "Odm Item Group Uid" - }, - "description": "The unique id of the ODM Item Group." - } - ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OdmItemGroupPatchInput" + } + } + } + }, "responses": { - "201": { + "200": { "description": "OK.", "content": { "application/json": { @@ -4805,7 +4099,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in draft status.\n- The library doesn't allow to approve ODM Item Group.\n", + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in draft status.\n- The ODM Item Group had been in 'Final' status before.\n- The library doesn't allow to edit draft versions.\n", "content": { "application/json": { "schema": { @@ -4825,15 +4119,78 @@ } } } + }, + "delete": { + "tags": [ + "ODM Item Groups" + ], + "summary": "Delete draft version of ODM Item Group", + "operationId": "delete_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__delete", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + }, + { + "BearerJwtAuth": [] + } + ], + "parameters": [ + { + "name": "odm_item_group_uid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The unique id of the ODM Item Group.", + "title": "Odm Item Group Uid" + }, + "description": "The unique id of the ODM Item Group." + } + ], + "responses": { + "204": { + "description": "No Content - The ODM Item Group was successfully deleted." + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in draft status.\n- The ODM Item Group was already in final state or is in use.\n- The library doesn't allow to delete ODM Item Group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found - An ODM Item Group with the specified 'odm_item_group_uid' could not be found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } } }, - "/concepts/odms/item-groups/{odm_item_group_uid}/activations": { - "delete": { + "/concepts/odms/item-groups/{odm_item_group_uid}/relationships": { + "get": { "tags": [ "ODM Item Groups" ], - "summary": " Inactivate final version of ODM Item Group", - "operationId": "inactivate_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__activations_delete", + "summary": "Get UIDs of a specific ODM Item Group's relationships", + "operationId": "get_active_relationships_concepts_odms_item_groups__odm_item_group_uid__relationships_get", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -4857,11 +4214,18 @@ ], "responses": { "200": { - "description": "OK.", + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OdmItemGroup" + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": "Response Get Active Relationships Concepts Odms Item Groups Odm Item Group Uid Relationships Get" } } } @@ -4876,8 +4240,8 @@ } } }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in final status.", + "404": { + "description": "Entity not found", "content": { "application/json": { "schema": { @@ -4886,8 +4250,8 @@ } } }, - "404": { - "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' could not be found.", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -4897,13 +4261,16 @@ } } } - }, - "post": { + } + }, + "/concepts/odms/item-groups/{odm_item_group_uid}/versions": { + "get": { "tags": [ "ODM Item Groups" ], - "summary": "Reactivate retired version of a ODM Item Group", - "operationId": "reactivate_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__activations_post", + "summary": "List version history for ODM Item Group", + "description": "State before:\n - uid must exist.\n\nBusiness logic:\n - List version history for ODM Item Groups.\n - The returned versions are ordered by start_date descending (newest entries first).\n\nState after:\n - No change\n\nPossible errors:\n - Invalid uid.", + "operationId": "get_odm_item_group_versions_concepts_odms_item_groups__odm_item_group_uid__versions_get", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -4927,11 +4294,15 @@ ], "responses": { "200": { - "description": "OK.", + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OdmItemGroup" + "type": "array", + "items": { + "$ref": "#/components/schemas/OdmItemGroup" + }, + "title": "Response Get Odm Item Group Versions Concepts Odms Item Groups Odm Item Group Uid Versions Get" } } } @@ -4946,8 +4317,8 @@ } } }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in retired status.", + "404": { + "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' wasn't found.", "content": { "application/json": { "schema": { @@ -4956,8 +4327,8 @@ } } }, - "404": { - "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' could not be found.", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -4967,15 +4338,14 @@ } } } - } - }, - "/concepts/odms/item-groups/{odm_item_group_uid}/items": { + }, "post": { "tags": [ "ODM Item Groups" ], - "summary": "Adds items to the ODM Item Group.", - "operationId": "add_item_to_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__items_post", + "summary": " Create a new version of ODM Item Group", + "description": "State before:\n - uid must exist and the ODM Item Group must be in status Final.\n\nBusiness logic:\n- The ODM Item Group is changed to a draft state.\n\nState after:\n - ODM Item Group changed status to Draft and assigned a new minor version number.\n - Audit trail entry must be made with action of creating a new draft version.\n\nPossible errors:\n - Invalid uid or status not Final.", + "operationId": "create_odm_item_group_version_concepts_odms_item_groups__odm_item_group_uid__versions_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -4997,35 +4367,21 @@ "description": "The unique id of the ODM Item Group." }, { - "name": "override", + "name": "cascade_new_version", "in": "query", "required": false, "schema": { "type": "boolean", - "description": "If true, all existing item relationships will be replaced with the provided item relationships.", + "description": "If true, all child elements will also get a new version.", "default": false, - "title": "Override" + "title": "Cascade New Version" }, - "description": "If true, all existing item relationships will be replaced with the provided item relationships." + "description": "If true, all child elements will also get a new version." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmItemGroupItemPostInput" - }, - "title": "Odm Item Group Item Post Input" - } - } - } - }, "responses": { "201": { - "description": "Created - The items were successfully added to the ODM Item Group.", + "description": "OK.", "content": { "application/json": { "schema": { @@ -5045,7 +4401,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The library doesn't allow to create ODM Item Groups.\n", "content": { "application/json": { "schema": { @@ -5055,7 +4411,7 @@ } }, "404": { - "description": "Not Found - The items with the specified 'odm_item_group_uid' wasn't found.", + "description": "Not Found - Reasons include e.g.: \n- The ODM Item Group is not in final status.\n- The ODM Item Group with the specified 'odm_item_group_uid' could not be found.", "content": { "application/json": { "schema": { @@ -5067,13 +4423,13 @@ } } }, - "/concepts/odms/item-groups/{odm_item_group_uid}/vendor-elements": { + "/concepts/odms/item-groups/{odm_item_group_uid}/approvals": { "post": { "tags": [ "ODM Item Groups" ], - "summary": "Adds ODM Vendor Elements to the ODM Item Group.", - "operationId": "add_vendor_elements_to_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__vendor_elements_post", + "summary": "Approve draft version of ODM Item Group", + "operationId": "approve_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__approvals_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -5093,37 +4449,11 @@ "title": "Odm Item Group Uid" }, "description": "The unique id of the ODM Item Group." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, "responses": { "201": { - "description": "Created - The ODM Vendor Elements were successfully added to the ODM Item Group.", + "description": "OK.", "content": { "application/json": { "schema": { @@ -5143,7 +4473,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in draft status.\n- The library doesn't allow to approve ODM Item Group.\n", "content": { "application/json": { "schema": { @@ -5153,7 +4483,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendor Elements with the specified 'odm_item_group_uid' wasn't found.", + "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' wasn't found.", "content": { "application/json": { "schema": { @@ -5165,13 +4495,13 @@ } } }, - "/concepts/odms/item-groups/{odm_item_group_uid}/vendor-attributes": { - "post": { + "/concepts/odms/item-groups/{odm_item_group_uid}/activations": { + "delete": { "tags": [ "ODM Item Groups" ], - "summary": "Adds ODM Vendor Attributes to the ODM Item Group.", - "operationId": "add_vendor_attributes_to_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__vendor_attributes_post", + "summary": " Inactivate final version of ODM Item Group", + "operationId": "inactivate_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__activations_delete", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -5191,37 +4521,11 @@ "title": "Odm Item Group Uid" }, "description": "The unique id of the ODM Item Group." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Attribute relationships will be replaced with the provided ODM Vendor Attribute relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Attribute relationships will be replaced with the provided ODM Vendor Attribute relationships." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, "responses": { - "201": { - "description": "Created - The ODM Vendor Attributes were successfully added to the ODM Item Group.", + "200": { + "description": "OK.", "content": { "application/json": { "schema": { @@ -5241,7 +4545,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in final status.", "content": { "application/json": { "schema": { @@ -5251,7 +4555,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendor Attributes with the specified 'odm_item_group_uid' wasn't found.", + "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' could not be found.", "content": { "application/json": { "schema": { @@ -5261,15 +4565,13 @@ } } } - } - }, - "/concepts/odms/item-groups/{odm_item_group_uid}/vendor-element-attributes": { + }, "post": { "tags": [ "ODM Item Groups" ], - "summary": "Adds ODM Vendor Element attributes to the ODM Item Group.", - "operationId": "add_vendor_element_attributes_to_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__vendor_element_attributes_post", + "summary": "Reactivate retired version of a ODM Item Group", + "operationId": "reactivate_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__activations_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -5289,37 +4591,11 @@ "title": "Odm Item Group Uid" }, "description": "The unique id of the ODM Item Group." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Element attribute relationships will be replaced with the provided ODM Vendor Element attribute relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Element attribute relationships will be replaced with the provided ODM Vendor Element attribute relationships." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, "responses": { - "201": { - "description": "Created - The ODM Vendor Element attributes were successfully added to the ODM Item Group.", + "200": { + "description": "OK.", "content": { "application/json": { "schema": { @@ -5339,7 +4615,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item Group is not in retired status.", "content": { "application/json": { "schema": { @@ -5349,7 +4625,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendor Element attributes with the specified 'odm_item_group_uid' wasn't found.", + "description": "Not Found - The ODM Item Group with the specified 'odm_item_group_uid' could not be found.", "content": { "application/json": { "schema": { @@ -5361,13 +4637,13 @@ } } }, - "/concepts/odms/item-groups/{odm_item_group_uid}/vendors": { + "/concepts/odms/item-groups/{odm_item_group_uid}/items": { "post": { "tags": [ "ODM Item Groups" ], - "summary": "Manages all ODM Vendors by replacing existing ODM Vendors by provided ODM Vendors.", - "operationId": "manage_vendors_of_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__vendors_post", + "summary": "Adds items to the ODM Item Group.", + "operationId": "add_item_to_odm_item_group_concepts_odms_item_groups__odm_item_group_uid__items_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -5387,6 +4663,18 @@ "title": "Odm Item Group Uid" }, "description": "The unique id of the ODM Item Group." + }, + { + "name": "override", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "If true, all existing item relationships will be replaced with the provided item relationships.", + "default": false, + "title": "Override" + }, + "description": "If true, all existing item relationships will be replaced with the provided item relationships." } ], "requestBody": { @@ -5394,17 +4682,23 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OdmVendorsPostInput" + "type": "array", + "items": { + "$ref": "#/components/schemas/OdmItemGroupItemPostInput" + }, + "title": "Odm Item Group Item Post Input" } } } }, "responses": { "201": { - "description": "Created - The ODM Vendors were successfully added to the ODM Item Group.", + "description": "Created - The items were successfully added to the ODM Item Group.", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/OdmItemGroup" + } } } }, @@ -5429,7 +4723,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendors with the specified 'odm_item_group_uid' wasn't found.", + "description": "Not Found - The items with the specified 'odm_item_group_uid' wasn't found.", "content": { "application/json": { "schema": { @@ -5572,11 +4866,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -6203,310 +5497,18 @@ ], "responses": { "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": "Response Get Active Relationships Concepts Odms Items Odm Item Uid Relationships Get" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Entity not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/items/{odm_item_uid}/versions": { - "get": { - "tags": [ - "ODM Items" - ], - "summary": "List version history for ODM Item", - "description": "State before:\n - uid must exist.\n\nBusiness logic:\n - List version history for ODM Items.\n - The returned versions are ordered by start_date descending (newest entries first).\n\nState after:\n - No change\n\nPossible errors:\n - Invalid uid.", - "operationId": "get_odm_item_versions_concepts_odms_items__odm_item_uid__versions_get", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item.", - "title": "Odm Item Uid" - }, - "description": "The unique id of the ODM Item." - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmItem" - }, - "title": "Response Get Odm Item Versions Concepts Odms Items Odm Item Uid Versions Get" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Item with the specified 'odm_item_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Bad Request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - }, - "post": { - "tags": [ - "ODM Items" - ], - "summary": " Create a new version of ODM Item", - "description": "State before:\n - uid must exist and the ODM Item must be in status Final.\n\nBusiness logic:\n- The ODM Item is changed to a draft state.\n\nState after:\n - ODM Item changed status to Draft and assigned a new minor version number.\n - Audit trail entry must be made with action of creating a new draft version.\n\nPossible errors:\n - Invalid uid or status not Final.", - "operationId": "create_odm_item_version_concepts_odms_items__odm_item_uid__versions_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item.", - "title": "Odm Item Uid" - }, - "description": "The unique id of the ODM Item." - } - ], - "responses": { - "201": { - "description": "OK.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmItem" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The library doesn't allow to create ODM Items.\n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - Reasons include e.g.: \n- The ODM Item is not in final status.\n- The ODM Item with the specified 'odm_item_uid' could not be found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/items/{odm_item_uid}/approvals": { - "post": { - "tags": [ - "ODM Items" - ], - "summary": "Approve draft version of ODM Item", - "operationId": "approve_odm_item_concepts_odms_items__odm_item_uid__approvals_post", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item.", - "title": "Odm Item Uid" - }, - "description": "The unique id of the ODM Item." - } - ], - "responses": { - "201": { - "description": "OK.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmItem" - } - } - } - }, - "403": { - "description": "Forbidden", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item is not in draft status.\n- The library doesn't allow to approve ODM Item.\n", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "404": { - "description": "Not Found - The ODM Item with the specified 'odm_item_uid' wasn't found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - } - } - }, - "/concepts/odms/items/{odm_item_uid}/activations": { - "delete": { - "tags": [ - "ODM Items" - ], - "summary": " Inactivate final version of ODM Item", - "operationId": "inactivate_odm_item_concepts_odms_items__odm_item_uid__activations_delete", - "security": [ - { - "OAuth2AuthorizationCodeBearer": [] - }, - { - "BearerJwtAuth": [] - } - ], - "parameters": [ - { - "name": "odm_item_uid", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "The unique id of the ODM Item.", - "title": "Odm Item Uid" - }, - "description": "The unique id of the ODM Item." - } - ], - "responses": { - "200": { - "description": "OK.", + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OdmItem" + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": "Response Get Active Relationships Concepts Odms Items Odm Item Uid Relationships Get" } } } @@ -6521,8 +5523,8 @@ } } }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item is not in final status.", + "404": { + "description": "Entity not found", "content": { "application/json": { "schema": { @@ -6531,8 +5533,8 @@ } } }, - "404": { - "description": "Not Found - The ODM Item with the specified 'odm_item_uid' could not be found.", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -6542,13 +5544,16 @@ } } } - }, - "post": { + } + }, + "/concepts/odms/items/{odm_item_uid}/versions": { + "get": { "tags": [ "ODM Items" ], - "summary": "Reactivate retired version of a ODM Item", - "operationId": "reactivate_odm_item_concepts_odms_items__odm_item_uid__activations_post", + "summary": "List version history for ODM Item", + "description": "State before:\n - uid must exist.\n\nBusiness logic:\n - List version history for ODM Items.\n - The returned versions are ordered by start_date descending (newest entries first).\n\nState after:\n - No change\n\nPossible errors:\n - Invalid uid.", + "operationId": "get_odm_item_versions_concepts_odms_items__odm_item_uid__versions_get", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -6572,11 +5577,15 @@ ], "responses": { "200": { - "description": "OK.", + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OdmItem" + "type": "array", + "items": { + "$ref": "#/components/schemas/OdmItem" + }, + "title": "Response Get Odm Item Versions Concepts Odms Items Odm Item Uid Versions Get" } } } @@ -6591,8 +5600,8 @@ } } }, - "400": { - "description": "Forbidden - Reasons include e.g.: \n- The ODM Item is not in retired status.", + "404": { + "description": "Not Found - The ODM Item with the specified 'odm_item_uid' wasn't found.", "content": { "application/json": { "schema": { @@ -6601,8 +5610,8 @@ } } }, - "404": { - "description": "Not Found - The ODM Item with the specified 'odm_item_uid' could not be found.", + "400": { + "description": "Bad Request", "content": { "application/json": { "schema": { @@ -6612,15 +5621,14 @@ } } } - } - }, - "/concepts/odms/items/{odm_item_uid}/vendor-elements": { + }, "post": { "tags": [ "ODM Items" ], - "summary": "Adds ODM Vendor Elements to the ODM Item.", - "operationId": "add_vendor_elements_to_odm_item_concepts_odms_items__odm_item_uid__vendor_elements_post", + "summary": " Create a new version of ODM Item", + "description": "State before:\n - uid must exist and the ODM Item must be in status Final.\n\nBusiness logic:\n- The ODM Item is changed to a draft state.\n\nState after:\n - ODM Item changed status to Draft and assigned a new minor version number.\n - Audit trail entry must be made with action of creating a new draft version.\n\nPossible errors:\n - Invalid uid or status not Final.", + "operationId": "create_odm_item_version_concepts_odms_items__odm_item_uid__versions_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -6640,37 +5648,11 @@ "title": "Odm Item Uid" }, "description": "The unique id of the ODM Item." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Element relationships will be replaced with the provided ODM Vendor Element relationships." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, "responses": { "201": { - "description": "Created - The ODM Vendor Elements were successfully added to the ODM Item.", + "description": "OK.", "content": { "application/json": { "schema": { @@ -6690,7 +5672,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The library doesn't allow to create ODM Items.\n", "content": { "application/json": { "schema": { @@ -6700,7 +5682,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendor Elements with the specified 'odm_item_uid' wasn't found.", + "description": "Not Found - Reasons include e.g.: \n- The ODM Item is not in final status.\n- The ODM Item with the specified 'odm_item_uid' could not be found.", "content": { "application/json": { "schema": { @@ -6712,13 +5694,13 @@ } } }, - "/concepts/odms/items/{odm_item_uid}/vendor-attributes": { + "/concepts/odms/items/{odm_item_uid}/approvals": { "post": { "tags": [ "ODM Items" ], - "summary": "Adds ODM Vendor Attributes to the ODM Item.", - "operationId": "add_vendor_attributes_to_odm_item_concepts_odms_items__odm_item_uid__vendor_attributes_post", + "summary": "Approve draft version of ODM Item", + "operationId": "approve_odm_item_concepts_odms_items__odm_item_uid__approvals_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -6738,37 +5720,11 @@ "title": "Odm Item Uid" }, "description": "The unique id of the ODM Item." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Attribute relationships will be replaced with the provided ODM Vendor Attribute relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Attribute relationships will be replaced with the provided ODM Vendor Attribute relationships." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, "responses": { "201": { - "description": "Created - The ODM Vendor Attributes were successfully added to the ODM Item.", + "description": "OK.", "content": { "application/json": { "schema": { @@ -6788,7 +5744,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item is not in draft status.\n- The library doesn't allow to approve ODM Item.\n", "content": { "application/json": { "schema": { @@ -6798,7 +5754,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendor Attributes with the specified 'odm_item_uid' wasn't found.", + "description": "Not Found - The ODM Item with the specified 'odm_item_uid' wasn't found.", "content": { "application/json": { "schema": { @@ -6810,13 +5766,13 @@ } } }, - "/concepts/odms/items/{odm_item_uid}/vendor-element-attributes": { - "post": { + "/concepts/odms/items/{odm_item_uid}/activations": { + "delete": { "tags": [ "ODM Items" ], - "summary": "Adds ODM Vendor Element attributes to the ODM Item.", - "operationId": "add_vendor_element_attributes_to_odm_item_concepts_odms_items__odm_item_uid__vendor_element_attributes_post", + "summary": " Inactivate final version of ODM Item", + "operationId": "inactivate_odm_item_concepts_odms_items__odm_item_uid__activations_delete", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -6836,37 +5792,11 @@ "title": "Odm Item Uid" }, "description": "The unique id of the ODM Item." - }, - { - "name": "override", - "in": "query", - "required": false, - "schema": { - "type": "boolean", - "description": "If true, all existing ODM Vendor Element attribute relationships will be replaced with the provided ODM Vendor Element attribute relationships.", - "default": false, - "title": "Override" - }, - "description": "If true, all existing ODM Vendor Element attribute relationships will be replaced with the provided ODM Vendor Element attribute relationships." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "title": "Odm Vendor Relation Post Input" - } - } - } - }, "responses": { - "201": { - "description": "Created - The ODM Vendor Element attributes were successfully added to the ODM Item.", + "200": { + "description": "OK.", "content": { "application/json": { "schema": { @@ -6886,7 +5816,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item is not in final status.", "content": { "application/json": { "schema": { @@ -6896,7 +5826,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendor Element attributes with the specified 'odm_item_uid' wasn't found.", + "description": "Not Found - The ODM Item with the specified 'odm_item_uid' could not be found.", "content": { "application/json": { "schema": { @@ -6906,15 +5836,13 @@ } } } - } - }, - "/concepts/odms/items/{odm_item_uid}/vendors": { + }, "post": { "tags": [ "ODM Items" ], - "summary": "Manages all ODM Vendors by replacing existing ODM Vendors by provided ODM Vendors.", - "operationId": "manage_vendors_of_odm_item_group_concepts_odms_items__odm_item_uid__vendors_post", + "summary": "Reactivate retired version of a ODM Item", + "operationId": "reactivate_odm_item_concepts_odms_items__odm_item_uid__activations_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -6936,19 +5864,9 @@ "description": "The unique id of the ODM Item." } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OdmVendorsPostInput" - } - } - } - }, "responses": { - "201": { - "description": "Created - The ODM Vendors were successfully added to the ODM Item.", + "200": { + "description": "OK.", "content": { "application/json": { "schema": { @@ -6968,7 +5886,7 @@ } }, "400": { - "description": "Forbidden - Reasons include e.g.: \n", + "description": "Forbidden - Reasons include e.g.: \n- The ODM Item is not in retired status.", "content": { "application/json": { "schema": { @@ -6978,7 +5896,7 @@ } }, "404": { - "description": "Not Found - The ODM Vendors with the specified 'odm_item_uid' wasn't found.", + "description": "Not Found - The ODM Item with the specified 'odm_item_uid' could not be found.", "content": { "application/json": { "schema": { @@ -7120,11 +6038,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -8239,11 +7157,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -9358,11 +8276,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -10467,11 +9385,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -11566,11 +10484,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "version", @@ -12579,6 +11497,143 @@ }, "description": "\nNumber of items to be returned per page.\n\nDefault: 10\n\nFunctionality: Provided together with `page_number`, selects the number of results per page.\n\nIn case the value is set to `0`, all rows will be returned.\n\nErrors: `page_number` not provided.\n" }, + { + "name": "sort_by", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "Comma separated list of fields to sort by. Available fields are: `name`, `context`. Prefix with `-` for descending order.\n E.g. `-name,context` sorts by name in descending order and then by context in ascending order.", + "default": "", + "title": "Sort By" + }, + "description": "Comma separated list of fields to sort by. Available fields are: `name`, `context`. Prefix with `-` for descending order.\n E.g. `-name,context` sorts by name in descending order and then by context in ascending order." + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Search by name or context. Search is case insensitive.", + "title": "Search" + }, + "description": "Search by name or context. Search is case insensitive." + }, + { + "name": "op", + "in": "query", + "required": false, + "schema": { + "enum": [ + "co", + "eq" + ], + "type": "string", + "description": "Operator to use for filtering. `co` for contains, `eq` for equals. Default is `co`.", + "default": "co", + "title": "Op" + }, + "description": "Operator to use for filtering. `co` for contains, `eq` for equals. Default is `co`." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomPage" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/concepts/odms/metadata/translated-texts": { + "get": { + "tags": [ + "ODM Metadata" + ], + "summary": "Listing of ODM Translated Texts", + "operationId": "get_translated_texts_concepts_odms_metadata_translated_texts_get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + }, + { + "BearerJwtAuth": [] + } + ], + "parameters": [ + { + "name": "page_number", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 1000000000, + "minimum": 1, + "description": "\nPage number of the returned list of entities.\n\nFunctionality : provided together with `page_size`, selects a page to retrieve for paginated results.\n\nErrors: `page_size` not provided, `page_number` must be equal or greater than 1.\n", + "default": 1, + "title": "Page Number" + }, + "description": "\nPage number of the returned list of entities.\n\nFunctionality : provided together with `page_size`, selects a page to retrieve for paginated results.\n\nErrors: `page_size` not provided, `page_number` must be equal or greater than 1.\n" + }, + { + "name": "page_size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 1000, + "minimum": 0, + "description": "\nNumber of items to be returned per page.\n\nDefault: 10\n\nFunctionality: Provided together with `page_number`, selects the number of results per page.\n\nIn case the value is set to `0`, all rows will be returned.\n\nErrors: `page_number` not provided.\n", + "default": 10, + "title": "Page Size" + }, + "description": "\nNumber of items to be returned per page.\n\nDefault: 10\n\nFunctionality: Provided together with `page_number`, selects the number of results per page.\n\nIn case the value is set to `0`, all rows will be returned.\n\nErrors: `page_number` not provided.\n" + }, + { + "name": "sort_by", + "in": "query", + "required": false, + "schema": { + "type": "string", + "description": "Comma separated list of fields to sort by. Available fields are: `text_type`, `language` and `text`. Prefix with `-` for descending order.\n E.g. `-language,text_type` sorts by language in descending order and then by text_type in ascending order.", + "default": "", + "title": "Sort By" + }, + "description": "Comma separated list of fields to sort by. Available fields are: `text_type`, `language` and `text`. Prefix with `-` for descending order.\n E.g. `-language,text_type` sorts by language in descending order and then by text_type in ascending order." + }, { "name": "search", "in": "query", @@ -12592,10 +11647,26 @@ "type": "null" } ], - "description": "Search by name or context. Search is case insensitive.", + "description": "Search by text_type, language or text. Search is case insensitive.", "title": "Search" }, - "description": "Search by name or context. Search is case insensitive." + "description": "Search by text_type, language or text. Search is case insensitive." + }, + { + "name": "op", + "in": "query", + "required": false, + "schema": { + "enum": [ + "co", + "eq" + ], + "type": "string", + "description": "Operator to use for filtering. `co` for contains, `eq` for equals. Default is `co`.", + "default": "co", + "title": "Op" + }, + "description": "Operator to use for filtering. `co` for contains, `eq` for equals. Default is `co`." } ], "responses": { @@ -12632,13 +11703,13 @@ } } }, - "/concepts/odms/metadata/descriptions": { + "/concepts/odms/metadata/formal-expressions": { "get": { "tags": [ "ODM Metadata" ], - "summary": "Listing of ODM Descriptions", - "operationId": "get_descriptions_concepts_odms_metadata_descriptions_get", + "summary": "Listing of ODM Formal Expressions", + "operationId": "get_formal_expressions_concepts_odms_metadata_formal_expressions_get", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -12677,16 +11748,16 @@ "description": "\nNumber of items to be returned per page.\n\nDefault: 10\n\nFunctionality: Provided together with `page_number`, selects the number of results per page.\n\nIn case the value is set to `0`, all rows will be returned.\n\nErrors: `page_number` not provided.\n" }, { - "name": "exclude_english", + "name": "sort_by", "in": "query", "required": false, "schema": { - "type": "boolean", - "description": "Exclude English descriptions (excludes `en` and `eng`).", - "default": false, - "title": "Exclude English" + "type": "string", + "description": "Comma separated list of fields to sort by. Available fields are: `context` and `expression`. Prefix with `-` for descending order.\n E.g. `-context,expression` sorts by context in descending order and then by expression in ascending order.", + "default": "", + "title": "Sort By" }, - "description": "Exclude English descriptions (excludes `en` and `eng`)." + "description": "Comma separated list of fields to sort by. Available fields are: `context` and `expression`. Prefix with `-` for descending order.\n E.g. `-context,expression` sorts by context in descending order and then by expression in ascending order." }, { "name": "search", @@ -12701,10 +11772,26 @@ "type": "null" } ], - "description": "Search by name, description, instruction or sponsor instruction. Search is case insensitive.", + "description": "Search by context or expression. Search is case insensitive.", "title": "Search" }, - "description": "Search by name, description, instruction or sponsor instruction. Search is case insensitive." + "description": "Search by context or expression. Search is case insensitive." + }, + { + "name": "op", + "in": "query", + "required": false, + "schema": { + "enum": [ + "co", + "eq" + ], + "type": "string", + "description": "Operator to use for filtering. `co` for contains, `eq` for equals. Default is `co`.", + "default": "co", + "title": "Op" + }, + "description": "Operator to use for filtering. `co` for contains, `eq` for equals. Default is `co`." } ], "responses": { @@ -12741,13 +11828,13 @@ } } }, - "/concepts/odms/metadata/formal-expressions": { - "get": { + "/concepts/odms/metadata/report": { + "post": { "tags": [ "ODM Metadata" ], - "summary": "Listing of ODM Formal Expressions", - "operationId": "get_formal_expressions_concepts_odms_metadata_formal_expressions_get", + "summary": "Export ODM Report", + "operationId": "get_odm_report_concepts_odms_metadata_report_post", "security": [ { "OAuth2AuthorizationCodeBearer": [] @@ -12758,65 +11845,47 @@ ], "parameters": [ { - "name": "page_size", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "maximum": 1000, - "minimum": 0, - "description": "\nNumber of items to be returned per page.\n\nDefault: 10\n\nFunctionality: Provided together with `page_number`, selects the number of results per page.\n\nIn case the value is set to `0`, all rows will be returned.\n\nErrors: `page_number` not provided.\n", - "default": 10, - "title": "Page Size" - }, - "description": "\nNumber of items to be returned per page.\n\nDefault: 10\n\nFunctionality: Provided together with `page_number`, selects the number of results per page.\n\nIn case the value is set to `0`, all rows will be returned.\n\nErrors: `page_number` not provided.\n" - }, - { - "name": "page_number", + "name": "target_type", "in": "query", - "required": false, + "required": true, "schema": { - "type": "integer", - "maximum": 1000000000, - "minimum": 1, - "description": "\nPage number of the returned list of entities.\n\nFunctionality : provided together with `page_size`, selects a page to retrieve for paginated results.\n\nErrors: `page_size` not provided, `page_number` must be equal or greater than 1.\n", - "default": 1, - "title": "Page Number" - }, - "description": "\nPage number of the returned list of entities.\n\nFunctionality : provided together with `page_size`, selects a page to retrieve for paginated results.\n\nErrors: `page_size` not provided, `page_number` must be equal or greater than 1.\n" + "$ref": "#/components/schemas/TargetType" + } }, { - "name": "search", + "name": "targets", "in": "query", - "required": false, + "required": true, "schema": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Search by context or expression. Search is case insensitive.", - "title": "Search" + "type": "array", + "items": { + "type": "string" + }, + "description": "List of UIDs and (optionally) versions separated by comma. E.g. `uid1,v1` or `uid1` for latest version.", + "title": "Targets" }, - "description": "Search by context or expression. Search is case insensitive." + "description": "List of UIDs and (optionally) versions separated by comma. E.g. `uid1,v1` or `uid1` for latest version." } ], "responses": { "200": { "description": "Successful Response", + "content": { + "application/html": {} + } + }, + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomPage" + "$ref": "#/components/schemas/ErrorResponse" } } } }, - "403": { - "description": "Forbidden", + "404": { + "description": "Entity not found", "content": { "application/json": { "schema": { @@ -13397,11 +12466,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -13809,11 +12878,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -15064,11 +14133,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -15453,11 +14522,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -16478,11 +15547,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -16794,11 +15863,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -17646,11 +16715,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -18048,11 +17117,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -19293,11 +18362,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -19685,11 +18754,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -20648,11 +19717,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -20964,11 +20033,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -21816,11 +20885,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -22218,11 +21287,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -23481,11 +22550,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -23797,11 +22866,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -24631,11 +23700,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -25023,11 +24092,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -25986,11 +25055,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -26388,11 +25457,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -27669,11 +26738,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -27985,11 +27054,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -28826,11 +27895,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -29215,11 +28284,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -30232,11 +29301,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -30634,11 +29703,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -31897,11 +30966,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -32213,11 +31282,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -33047,11 +32116,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -33439,11 +32508,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -34409,11 +33478,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -34811,11 +33880,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -35850,11 +34919,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -36239,11 +35308,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -38178,11 +37247,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "term_filter", @@ -38361,11 +37430,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -38952,11 +38021,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -39306,11 +38375,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -39801,11 +38870,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -40657,11 +39726,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -41615,11 +40684,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -42500,11 +41569,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -43624,11 +42693,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -44737,11 +43806,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -45699,11 +44768,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -46731,11 +45800,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -47476,11 +46545,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -47758,11 +46827,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -49108,11 +48177,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -50506,11 +49575,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "sort_by", @@ -50647,11 +49716,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "sort_by", @@ -50823,11 +49892,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -51520,6 +50589,18 @@ }, "description": "Optionally, the name of a CT Catalogue to filter Codelists." }, + { + "name": "valid_codelists_for_item", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Whether to look for codelists using the Valid codelists relationship.\n\nif set to True, this will take precedence over SDTMIG and Sponsor Model.\n\nDefaults to False.", + "default": false, + "title": "Valid Codelists For Item" + }, + "description": "Whether to look for codelists using the Valid codelists relationship.\n\nif set to True, this will take precedence over SDTMIG and Sponsor Model.\n\nDefaults to False." + }, { "name": "sort_by", "in": "query", @@ -51618,11 +50699,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -51743,6 +50824,90 @@ } } }, + "/activity-item-classes/{activity_item_class_uid}/valid-codelist-mappings": { + "patch": { + "tags": [ + "Activity Item Classes" + ], + "summary": "Edit the mappings to valid codelists for ActivityItems", + "description": "State before:\n- uid must exist\n\nBusiness logic:\n- Mappings to valid codelists are replaced with the provided ones\n\nPossible errors:\n- Invalid uid", + "operationId": "patch_valid_codelist_mappings_activity_item_classes__activity_item_class_uid__valid_codelist_mappings_patch", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + }, + { + "BearerJwtAuth": [] + } + ], + "parameters": [ + { + "name": "activity_item_class_uid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The unique id of the ActivityItemClass", + "title": "Activity Item Class Uid" + }, + "description": "The unique id of the ActivityItemClass" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidCodelistMappingInput", + "description": "The uid of valid codelists to map activity item class to." + } + } + } + }, + "responses": { + "200": { + "description": "OK.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivityItemClass" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found - Reasons include e.g.: \n- The activity item class with the specified 'activity_item_class_uid' could not be found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, "/activity-item-classes/{activity_item_class_uid}/approvals": { "post": { "tags": [ @@ -52131,11 +51296,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -52313,11 +51478,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -52543,11 +51708,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -52725,11 +51890,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -53659,11 +52824,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -53889,11 +53054,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -54823,11 +53988,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -55053,11 +54218,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -55987,11 +55152,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -56217,11 +55382,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -57151,11 +56316,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -57381,11 +56546,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -58426,11 +57591,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -58747,11 +57912,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -59496,11 +58661,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -60328,11 +59493,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -60558,11 +59723,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -61317,11 +60482,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -61438,11 +60603,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -61911,11 +61076,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -62151,11 +61316,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -62940,11 +62105,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -63494,11 +62659,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -63976,11 +63141,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -64458,11 +63623,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -64940,11 +64105,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -65422,11 +64587,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -65904,11 +65069,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -66386,11 +65551,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -66868,11 +66033,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -67350,11 +66515,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -67832,11 +66997,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -68297,11 +67462,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -68888,11 +68053,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -71277,11 +70442,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "deleted", @@ -71620,11 +70785,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -72905,11 +72070,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -75575,11 +74740,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -76267,25 +75432,91 @@ "responses": { "200": { "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - }, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {} - } - }, - "403": { - "description": "Forbidden", + "content": { + "application/json": { + "schema": {} + }, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {} + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Entity not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/studies/{study_uid}/odm-forms": { + "get": { + "tags": [ + "Study Selections" + ], + "summary": "Get a paginated list of study data suppliers of a study", + "operationId": "get_a_paginated_list_of_study_crfs_of_a_study_studies__study_uid__odm_forms_get", + "security": [ + { + "OAuth2AuthorizationCodeBearer": [] + }, + { + "BearerJwtAuth": [] + } + ], + "parameters": [ + { + "name": "study_uid", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "The unique id of the study.", + "title": "Study Uid" + }, + "description": "The unique id of the study." + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorResponse" + "type": "array", + "items": { + "type": "object" + }, + "title": "Response Get A Paginated List Of Study Crfs Of A Study Studies Study Uid Odm Forms Get" } } } }, - "404": { - "description": "Entity not found", + "403": { + "description": "Forbidden", "content": { "application/json": { "schema": { @@ -76421,11 +75652,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -76778,11 +76009,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -77748,11 +76979,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -78126,11 +77357,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -79463,11 +78694,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -79841,11 +79072,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -81252,11 +80483,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -81627,11 +80858,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -82989,11 +82220,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -83367,11 +82598,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -84940,11 +84171,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -85201,11 +84432,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -86555,11 +85786,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -86795,11 +86026,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -87999,11 +87230,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -88384,11 +87615,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -88769,11 +88000,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -89793,11 +89024,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -90258,11 +89489,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -91132,11 +90363,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -92205,11 +91436,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -93182,11 +92413,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "arm_uid", @@ -94099,11 +93330,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -94986,11 +94217,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -97144,11 +96375,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -98117,11 +97348,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -99357,11 +98588,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -99523,11 +98754,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -101922,11 +101153,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -101946,6 +101177,18 @@ "title": "Study Value Version" }, "description": "If specified, study data with specified version is returned.\n\n Only exact matches are considered. \n\n E.g. 1, 2, 2.1, ..." + }, + { + "name": "derive_props_based_on_timeline", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Indicates whether the (visit_name, visit_short_name, unique_visit_number) properties are derived based on the study visit timeline and not from database values.", + "default": false, + "title": "Derive Props Based On Timeline" + }, + "description": "Indicates whether the (visit_name, visit_short_name, unique_visit_number) properties are derived based on the study visit timeline and not from database values." } ], "responses": { @@ -102717,6 +101960,18 @@ "title": "Study Value Version" }, "description": "If specified, study data with specified version is returned.\n\n Only exact matches are considered. \n\n E.g. 1, 2, 2.1, ..." + }, + { + "name": "derive_props_based_on_timeline", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Indicates whether the (visit_name, visit_short_name, unique_visit_number) properties are derived based on the study visit timeline and not from database values.", + "default": false, + "title": "Derive Props Based On Timeline" + }, + "description": "Indicates whether the (visit_name, visit_short_name, unique_visit_number) properties are derived based on the study visit timeline and not from database values." } ], "responses": { @@ -103690,11 +102945,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -104820,11 +104075,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -105158,11 +104413,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -105512,11 +104767,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -105866,11 +105121,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -106238,11 +105493,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -106610,11 +105865,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -106939,11 +106194,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -107134,11 +106389,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -107329,11 +106584,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -107524,11 +106779,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -107719,11 +106974,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -107914,11 +107169,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -108119,11 +107374,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." }, { "name": "study_value_version", @@ -109401,11 +108656,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -109793,11 +109048,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -110185,11 +109440,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -110553,11 +109808,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -110921,11 +110176,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -111289,11 +110544,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -111703,11 +110958,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -112161,11 +111416,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -112630,11 +111885,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -113128,11 +112383,11 @@ "required": false, "schema": { "type": "boolean", - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n", + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page.", "default": false, "title": "Total Count" }, - "description": "Boolean value specifying whether total count of entities should be included in the response.\n\nFunctionality: retrieve total count of queried entities.\n\n" + "description": "Boolean flag to include the total count of entities in the response.\n\nDefault: `false`\n\nFunctionality: When set to `true`, returns the total number of entities that match the query.\n\nWhen combined with filters, the count reflects only the entities matching those filters.\n\nNote: This operation can be expensive for large datasets.\n\nSpecial case: A value of `-1` indicates that the exact count is unavailable due to performance constraints, but confirms that at least one more entity exists beyond the current page." } ], "responses": { @@ -118317,6 +117572,39 @@ } ], "title": "Value Dependent Map" + }, + "activity_instance_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Activity Instance Name" + }, + "activity_instance_adam_param_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Activity Instance Adam Param Code" + }, + "activity_instance_topic_code": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Activity Instance Topic Code" } }, "type": "object", @@ -118324,10 +117612,94 @@ "activity_instance_uid", "activity_item_class_uid", "odm_form_uid", - "odm_item_group_uid" + "odm_item_group_uid", + "activity_instance_name", + "activity_instance_adam_param_code", + "activity_instance_topic_code" ], "title": "ActivityInstanceRel" }, + "ActivityInstanceRelInput": { + "properties": { + "activity_instance_uid": { + "type": "string", + "minLength": 1, + "title": "Activity Instance Uid" + }, + "activity_item_class_uid": { + "type": "string", + "minLength": 1, + "title": "Activity Item Class Uid" + }, + "odm_form_uid": { + "type": "string", + "minLength": 1, + "title": "Odm Form Uid" + }, + "odm_item_group_uid": { + "type": "string", + "minLength": 1, + "title": "Odm Item Group Uid" + }, + "order": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Order" + }, + "primary": { + "type": "boolean", + "title": "Primary", + "default": false + }, + "preset_response_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Preset Response Value" + }, + "value_condition": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value Condition" + }, + "value_dependent_map": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Value Dependent Map" + } + }, + "type": "object", + "required": [ + "activity_instance_uid", + "activity_item_class_uid", + "odm_form_uid", + "odm_item_group_uid" + ], + "title": "ActivityInstanceRelInput" + }, "ActivityInstruction": { "properties": { "uid": { @@ -120226,6 +119598,17 @@ "is_adam_param_specific": { "type": "boolean", "title": "Is Adam Param Specific" + }, + "text_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Text Value" } }, "type": "object", @@ -120919,6 +120302,17 @@ "is_adam_param_specific": { "type": "boolean", "title": "Is Adam Param Specific" + }, + "text_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Text Value" } }, "type": "object", @@ -122280,9 +121674,9 @@ "type": "boolean", "title": "Extensible" }, - "ordinal": { + "is_ordinal": { "type": "boolean", - "title": "Ordinal" + "title": "Is Ordinal" }, "sponsor_preferred_name": { "type": "string", @@ -122313,7 +121707,7 @@ "submission_value", "definition", "extensible", - "ordinal", + "is_ordinal", "sponsor_preferred_name", "template_parameter", "library_name" @@ -122388,9 +121782,9 @@ "type": "boolean", "title": "Extensible" }, - "ordinal": { + "is_ordinal": { "type": "boolean", - "title": "Ordinal" + "title": "Is Ordinal" }, "library_name": { "anyOf": [ @@ -122493,7 +121887,7 @@ "submission_value", "definition", "extensible", - "ordinal" + "is_ordinal" ], "title": "CTCodelistAttributes" }, @@ -122558,7 +121952,7 @@ ], "title": "Extensible" }, - "ordinal": { + "is_ordinal": { "anyOf": [ { "type": "boolean" @@ -122567,7 +121961,7 @@ "type": "null" } ], - "title": "Ordinal" + "title": "Is Ordinal" }, "change_description": { "type": "string", @@ -122599,6 +121993,18 @@ "title": "Name", "nullable": true }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version", + "nullable": true + }, "submission_value": { "anyOf": [ { @@ -122624,6 +122030,7 @@ "nullable": true } }, + "additionalProperties": true, "type": "object", "required": [ "uid" @@ -122698,9 +122105,9 @@ "type": "boolean", "title": "Extensible" }, - "ordinal": { + "is_ordinal": { "type": "boolean", - "title": "Ordinal" + "title": "Is Ordinal" }, "library_name": { "anyOf": [ @@ -122811,7 +122218,7 @@ "submission_value", "definition", "extensible", - "ordinal" + "is_ordinal" ], "title": "CTCodelistAttributesVersion", "description": "Class for storing CTCodelistAttributes and calculation of differences" @@ -122856,9 +122263,9 @@ "type": "boolean", "title": "Extensible" }, - "ordinal": { + "is_ordinal": { "type": "boolean", - "title": "Ordinal" + "title": "Is Ordinal" }, "sponsor_preferred_name": { "type": "string", @@ -122925,7 +122332,7 @@ "submission_value", "definition", "extensible", - "ordinal", + "is_ordinal", "sponsor_preferred_name", "template_parameter", "terms", @@ -123401,6 +122808,18 @@ "title": "Order", "nullable": true }, + "ordinal": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Ordinal", + "nullable": true + }, "library_name": { "anyOf": [ { @@ -123526,6 +122945,17 @@ "title": "Order", "default": 999999 }, + "ordinal": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Ordinal" + }, "submission_value": { "type": "string", "minLength": 1, @@ -124842,6 +124272,18 @@ "title": "Order", "nullable": true }, + "ordinal": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Ordinal", + "nullable": true + }, "start_date": { "type": "string", "format": "date-time", @@ -124885,6 +124327,17 @@ } ], "title": "Order" + }, + "ordinal": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "title": "Ordinal" } }, "type": "object", @@ -126675,6 +126128,11 @@ "title": "Short Name", "description": "short name for the study arm" }, + "label": { + "type": "string", + "title": "Label", + "description": "label for the study arm" + }, "number_of_subjects": { "anyOf": [ { @@ -126701,6 +126159,7 @@ "uid", "name", "short_name", + "label", "study_cohorts" ], "title": "CompactStudyArm" @@ -130031,7 +129490,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130066,7 +129525,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130100,7 +129559,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130134,7 +129593,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130168,7 +129627,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130202,7 +129661,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130236,7 +129695,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130270,7 +129729,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130304,7 +129763,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130338,7 +129797,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130372,7 +129831,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130406,7 +129865,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130440,7 +129899,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130474,7 +129933,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130508,7 +129967,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130542,7 +130001,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130576,7 +130035,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130610,7 +130069,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130644,7 +130103,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130678,7 +130137,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130712,7 +130171,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130746,7 +130205,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130780,7 +130239,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130814,7 +130273,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130848,7 +130307,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130882,7 +130341,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130916,7 +130375,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130950,7 +130409,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -130984,7 +130443,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131018,7 +130477,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131052,7 +130511,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131086,7 +130545,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131120,7 +130579,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131154,7 +130613,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131188,7 +130647,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131222,7 +130681,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131256,7 +130715,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131290,7 +130749,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131324,7 +130783,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131358,7 +130817,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131392,7 +130851,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131426,7 +130885,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131460,7 +130919,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131494,7 +130953,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131528,7 +130987,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131562,7 +131021,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131596,7 +131055,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131630,7 +131089,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131664,7 +131123,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131698,7 +131157,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131732,7 +131191,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131766,7 +131225,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131800,7 +131259,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131834,7 +131293,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131868,7 +131327,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131902,7 +131361,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131936,7 +131395,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -131970,7 +131429,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132004,7 +131463,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132038,7 +131497,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132072,7 +131531,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132106,7 +131565,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132140,7 +131599,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132174,7 +131633,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132208,7 +131667,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132242,7 +131701,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132276,7 +131735,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132310,7 +131769,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132344,7 +131803,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132378,7 +131837,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132412,7 +131871,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132446,7 +131905,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132480,7 +131939,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132514,7 +131973,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132548,7 +132007,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132582,7 +132041,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132616,7 +132075,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132650,7 +132109,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132684,7 +132143,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132718,7 +132177,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132752,7 +132211,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132786,7 +132245,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132820,7 +132279,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132854,7 +132313,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132888,7 +132347,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132922,7 +132381,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132956,7 +132415,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -132990,7 +132449,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133024,7 +132483,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133058,7 +132517,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133092,7 +132551,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133126,7 +132585,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133160,7 +132619,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133194,7 +132653,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133228,7 +132687,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133262,7 +132721,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133296,7 +132755,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133330,7 +132789,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133364,7 +132823,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133398,7 +132857,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133432,7 +132891,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133466,7 +132925,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133500,7 +132959,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133534,7 +132993,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133568,7 +133027,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133602,7 +133061,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133636,7 +133095,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133670,7 +133129,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133711,7 +133170,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133755,7 +133214,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133789,7 +133248,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133823,7 +133282,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -133857,7 +133316,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" }, "page": { @@ -140511,7 +139970,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140533,7 +139992,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140555,7 +140014,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140577,7 +140036,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140599,7 +140058,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140621,7 +140080,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140643,7 +140102,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140665,7 +140124,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140687,7 +140146,7 @@ }, "total": { "type": "integer", - "minimum": 0.0, + "minimum": -1.0, "title": "Total" } }, @@ -140978,6 +140437,29 @@ "type": "object", "title": "HighLevelStudyDesignJsonModel" }, + "ISOLanguageModel": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "_1": { + "type": "string", + "title": "1" + }, + "_2T": { + "type": "string", + "title": "2T" + } + }, + "type": "object", + "required": [ + "name", + "_1", + "_2T" + ], + "title": "ISOLanguageModel" + }, "IndexedTemplateParameterTerm": { "properties": { "uid": { @@ -144725,12 +144207,12 @@ "type": "array", "title": "Formal Expressions" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -144758,7 +144240,7 @@ "library_name", "oid", "formal_expressions", - "descriptions", + "translated_texts", "aliases", "possible_actions" ], @@ -144795,12 +144277,12 @@ "type": "array", "title": "Formal Expressions" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -144816,7 +144298,7 @@ "name", "oid", "formal_expressions", - "descriptions", + "translated_texts", "aliases" ], "title": "OdmConditionPatchInput" @@ -144853,12 +144335,12 @@ "type": "array", "title": "Formal Expressions" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -144872,70 +144354,11 @@ "required": [ "name", "formal_expressions", - "descriptions", + "translated_texts", "aliases" ], "title": "OdmConditionPostInput" }, - "OdmDescriptionModel": { - "properties": { - "name": { - "type": "string", - "minLength": 1, - "title": "Name" - }, - "language": { - "type": "string", - "minLength": 1, - "title": "Language" - }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "format": "html", - "title": "Description", - "nullable": true - }, - "instruction": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "format": "html", - "title": "Instruction", - "nullable": true - }, - "sponsor_instruction": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "format": "html", - "title": "Sponsor Instruction", - "nullable": true - } - }, - "type": "object", - "required": [ - "name", - "language" - ], - "title": "OdmDescriptionModel" - }, "OdmElementWithParentUid": { "properties": { "uid": { @@ -145056,12 +144479,12 @@ "title": "Sdtm Version", "nullable": true }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -145115,7 +144538,7 @@ "uid", "name", "library_name", - "descriptions", + "translated_texts", "aliases", "item_groups", "vendor_elements", @@ -145212,12 +144635,12 @@ ], "title": "Repeating" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -145225,6 +144648,27 @@ }, "type": "array", "title": "Aliases" + }, + "vendor_elements": { + "items": { + "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" + }, + "type": "array", + "title": "Vendor Elements" + }, + "vendor_element_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Element Attributes" + }, + "vendor_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Attributes" } }, "type": "object", @@ -145234,7 +144678,10 @@ "oid", "sdtm_version", "repeating", - "descriptions" + "translated_texts", + "vendor_elements", + "vendor_element_attributes", + "vendor_attributes" ], "title": "OdmFormPatchInput" }, @@ -145279,12 +144726,12 @@ "minLength": 1, "title": "Repeating" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -145292,13 +144739,34 @@ }, "type": "array", "title": "Aliases" + }, + "vendor_elements": { + "items": { + "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" + }, + "type": "array", + "title": "Vendor Elements" + }, + "vendor_element_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Element Attributes" + }, + "vendor_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Attributes" } }, "type": "object", "required": [ "name", "repeating", - "descriptions" + "translated_texts" ], "title": "OdmFormPostInput" }, @@ -145308,6 +144776,18 @@ "type": "string", "title": "Uid" }, + "oid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Oid", + "nullable": true + }, "name": { "anyOf": [ { @@ -145566,12 +145046,12 @@ "title": "Comment", "nullable": true }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -145606,7 +145086,9 @@ "title": "Terms" }, "activity_instances": { - "items": {}, + "items": { + "$ref": "#/components/schemas/ActivityInstanceRel" + }, "type": "array", "title": "Activity Instances" }, @@ -145649,7 +145131,7 @@ "name", "library_name", "oid", - "descriptions", + "translated_texts", "aliases", "unit_definitions", "terms", @@ -145661,6 +145143,25 @@ ], "title": "OdmItem" }, + "OdmItemCodelist": { + "properties": { + "uid": { + "type": "string", + "minLength": 1, + "title": "Uid" + }, + "allows_multi_choice": { + "type": "boolean", + "title": "Allows Multi Choice", + "default": false + } + }, + "type": "object", + "required": [ + "uid" + ], + "title": "OdmItemCodelist" + }, "OdmItemGroup": { "properties": { "start_date": { @@ -145802,12 +145303,12 @@ "title": "Comment", "nullable": true }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -145869,7 +145370,7 @@ "name", "library_name", "oid", - "descriptions", + "translated_texts", "aliases", "sdtm_domains", "items", @@ -146065,12 +145566,12 @@ ], "title": "Comment" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -146085,6 +145586,27 @@ }, "type": "array", "title": "Sdtm Domain Uids" + }, + "vendor_elements": { + "items": { + "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" + }, + "type": "array", + "title": "Vendor Elements" + }, + "vendor_element_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Element Attributes" + }, + "vendor_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Attributes" } }, "type": "object", @@ -146098,8 +145620,11 @@ "origin", "purpose", "comment", - "descriptions", - "sdtm_domain_uids" + "translated_texts", + "sdtm_domain_uids", + "vendor_elements", + "vendor_element_attributes", + "vendor_attributes" ], "title": "OdmItemGroupPatchInput" }, @@ -146188,12 +145713,12 @@ ], "title": "Comment" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -146208,13 +145733,34 @@ }, "type": "array", "title": "Sdtm Domain Uids" + }, + "vendor_elements": { + "items": { + "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" + }, + "type": "array", + "title": "Vendor Elements" + }, + "vendor_element_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Element Attributes" + }, + "vendor_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Attributes" } }, "type": "object", "required": [ "name", "repeating", - "descriptions", + "translated_texts", "sdtm_domain_uids" ], "title": "OdmItemGroupPostInput" @@ -146418,12 +145964,12 @@ ], "title": "Comment" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -146439,17 +145985,15 @@ "type": "array", "title": "Unit Definitions" }, - "codelist_uid": { + "codelist": { "anyOf": [ { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/OdmItemCodelist" }, { "type": "null" } - ], - "title": "Codelist Uid" + ] }, "terms": { "items": { @@ -146458,9 +146002,30 @@ "type": "array", "title": "Terms" }, + "vendor_elements": { + "items": { + "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" + }, + "type": "array", + "title": "Vendor Elements" + }, + "vendor_element_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Element Attributes" + }, + "vendor_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Attributes" + }, "activity_instances": { "items": { - "$ref": "#/components/schemas/ActivityInstanceRel" + "$ref": "#/components/schemas/ActivityInstanceRelInput" }, "type": "array", "title": "Activity Instances" @@ -146479,8 +146044,11 @@ "sds_var_name", "origin", "comment", - "codelist_uid", - "terms" + "codelist", + "terms", + "vendor_elements", + "vendor_element_attributes", + "vendor_attributes" ], "title": "OdmItemPatchInput" }, @@ -146595,12 +146163,12 @@ ], "title": "Comment" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -146609,17 +146177,15 @@ "type": "array", "title": "Aliases" }, - "codelist_uid": { + "codelist": { "anyOf": [ { - "type": "string", - "minLength": 1 + "$ref": "#/components/schemas/OdmItemCodelist" }, { "type": "null" } - ], - "title": "Codelist Uid" + ] }, "unit_definitions": { "items": { @@ -146634,6 +146200,27 @@ }, "type": "array", "title": "Terms" + }, + "vendor_elements": { + "items": { + "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" + }, + "type": "array", + "title": "Vendor Elements" + }, + "vendor_element_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Element Attributes" + }, + "vendor_attributes": { + "items": { + "$ref": "#/components/schemas/OdmVendorRelationPostInput" + }, + "type": "array", + "title": "Vendor Attributes" } }, "type": "object", @@ -146806,8 +146393,7 @@ "type": "null" } ], - "title": "Order", - "default": 999999 + "title": "Order" }, "display_text": { "anyOf": [ @@ -146954,6 +146540,18 @@ "title": "Name", "nullable": true }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Version", + "nullable": true + }, "mandatory": { "type": "boolean", "title": "Mandatory", @@ -147087,12 +146685,12 @@ "type": "array", "title": "Formal Expressions" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -147121,7 +146719,7 @@ "oid", "method_type", "formal_expressions", - "descriptions", + "translated_texts", "aliases", "possible_actions" ], @@ -147170,12 +146768,12 @@ "type": "array", "title": "Formal Expressions" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -147192,7 +146790,7 @@ "oid", "method_type", "formal_expressions", - "descriptions" + "translated_texts" ], "title": "OdmMethodPatchInput" }, @@ -147240,12 +146838,12 @@ "type": "array", "title": "Formal Expressions" }, - "descriptions": { + "translated_texts": { "items": { - "$ref": "#/components/schemas/OdmDescriptionModel" + "$ref": "#/components/schemas/OdmTranslatedTextModel" }, "type": "array", - "title": "Descriptions" + "title": "Translated Texts" }, "aliases": { "items": { @@ -147259,7 +146857,7 @@ "required": [ "name", "formal_expressions", - "descriptions" + "translated_texts" ], "title": "OdmMethodPostInput" }, @@ -147702,6 +147300,41 @@ ], "title": "OdmStudyEventPostInput" }, + "OdmTranslatedTextModel": { + "properties": { + "text_type": { + "$ref": "#/components/schemas/OdmTranslatedTextTypeEnum" + }, + "language": { + "type": "string", + "minLength": 1, + "title": "Language" + }, + "text": { + "type": "string", + "format": "html", + "title": "Text" + } + }, + "type": "object", + "required": [ + "text_type", + "language", + "text" + ], + "title": "OdmTranslatedTextModel" + }, + "OdmTranslatedTextTypeEnum": { + "type": "string", + "enum": [ + "Description", + "Question", + "osb:DisplayText", + "osb:DesignNotes", + "osb:CompletionInstructions" + ], + "title": "OdmTranslatedTextTypeEnum" + }, "OdmVendorAttribute": { "properties": { "start_date": { @@ -148779,38 +148412,6 @@ ], "title": "OdmVendorRelationPostInput" }, - "OdmVendorsPostInput": { - "properties": { - "elements": { - "items": { - "$ref": "#/components/schemas/OdmVendorElementRelationPostInput" - }, - "type": "array", - "title": "Elements" - }, - "element_attributes": { - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "type": "array", - "title": "Element Attributes" - }, - "attributes": { - "items": { - "$ref": "#/components/schemas/OdmVendorRelationPostInput" - }, - "type": "array", - "title": "Attributes" - } - }, - "type": "object", - "required": [ - "elements", - "element_attributes", - "attributes" - ], - "title": "OdmVendorsPostInput" - }, "PharmaceuticalProduct": { "properties": { "start_date": { @@ -151836,6 +151437,17 @@ "is_adam_param_specific": { "type": "boolean", "title": "Is Adam Param Specific" + }, + "text_value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Text Value" } }, "type": "object", @@ -152323,6 +151935,7 @@ "nullable": true } }, + "additionalProperties": true, "type": "object", "title": "SponsorModelDataset" }, @@ -152574,6 +152187,7 @@ "default": "CDISC" } }, + "additionalProperties": true, "type": "object", "required": [ "dataset_uid", @@ -153000,6 +152614,7 @@ "nullable": true } }, + "additionalProperties": true, "type": "object", "title": "SponsorModelDatasetVariable" }, @@ -153416,6 +153031,7 @@ "default": "CDISC" } }, + "additionalProperties": true, "type": "object", "required": [ "dataset_uid", @@ -153502,7 +153118,7 @@ "show_activity_group_in_protocol_flowchart": { "type": "boolean", "title": "Show Activity Group In Protocol Flowchart", - "description": "show activity group in protocol flow chart", + "description": "show activity group in protocol flowchart", "nullable": true }, "study_uid": { @@ -153592,7 +153208,7 @@ "show_activity_group_in_protocol_flowchart": { "type": "boolean", "title": "Show Activity Group In Protocol Flowchart", - "description": "show activity group in protocol flow chart", + "description": "show activity group in protocol flowchart", "default": false, "nullable": true } @@ -154160,7 +153776,7 @@ "show_activity_subgroup_in_protocol_flowchart": { "type": "boolean", "title": "Show Activity Subgroup In Protocol Flowchart", - "description": "show activity subgroup in protocol flow chart", + "description": "show activity subgroup in protocol flowchart", "nullable": true }, "study_uid": { @@ -154250,7 +153866,7 @@ "show_activity_subgroup_in_protocol_flowchart": { "type": "boolean", "title": "Show Activity Subgroup In Protocol Flowchart", - "description": "show activity subgroup in protocol flow chart", + "description": "show activity subgroup in protocol flowchart", "default": false, "nullable": true } @@ -159605,7 +159221,7 @@ } ], "title": "Show Activity In Protocol Flowchart", - "description": "show activity in protocol flow chart", + "description": "show activity in protocol flowchart", "nullable": true }, "show_activity_subgroup_in_protocol_flowchart": { @@ -159618,7 +159234,7 @@ } ], "title": "Show Activity Subgroup In Protocol Flowchart", - "description": "show activity subgroup in protocol flow chart", + "description": "show activity subgroup in protocol flowchart", "nullable": true }, "show_activity_group_in_protocol_flowchart": { @@ -159631,13 +159247,13 @@ } ], "title": "Show Activity Group In Protocol Flowchart", - "description": "show activity group in protocol flow chart", + "description": "show activity group in protocol flowchart", "nullable": true }, "show_soa_group_in_protocol_flowchart": { "type": "boolean", "title": "Show Soa Group In Protocol Flowchart", - "description": "show soa group in protocol flow chart", + "description": "show soa group in protocol flowchart", "default": false }, "keep_old_version": { @@ -159973,7 +159589,7 @@ } ], "title": "Show Activity In Protocol Flowchart", - "description": "show activity in protocol flow chart", + "description": "show activity in protocol flowchart", "nullable": true }, "show_activity_subgroup_in_protocol_flowchart": { @@ -159986,7 +159602,7 @@ } ], "title": "Show Activity Subgroup In Protocol Flowchart", - "description": "show activity subgroup in protocol flow chart", + "description": "show activity subgroup in protocol flowchart", "nullable": true }, "show_activity_group_in_protocol_flowchart": { @@ -159999,13 +159615,13 @@ } ], "title": "Show Activity Group In Protocol Flowchart", - "description": "show activity group in protocol flow chart", + "description": "show activity group in protocol flowchart", "nullable": true }, "show_soa_group_in_protocol_flowchart": { "type": "boolean", "title": "Show Soa Group In Protocol Flowchart", - "description": "show soa group in protocol flow chart", + "description": "show soa group in protocol flowchart", "default": false }, "keep_old_version": { @@ -160185,6 +159801,12 @@ } ], "title": "Activity Instance Uid" + }, + "show_activity_in_protocol_flowchart": { + "type": "boolean", + "title": "Show Activity In Protocol Flowchart", + "description": "show activity in protocol flowchart", + "default": false } }, "type": "object", @@ -160322,7 +159944,7 @@ "show_activity_instance_in_protocol_flowchart": { "type": "boolean", "title": "Show Activity Instance In Protocol Flowchart", - "description": "show activity instance in operational flow chart" + "description": "show activity instance in operational flowchart" }, "keep_old_version": { "type": "boolean", @@ -160772,7 +160394,7 @@ "show_activity_instance_in_protocol_flowchart": { "type": "boolean", "title": "Show Activity Instance In Protocol Flowchart", - "description": "show activity instance in operational flow chart", + "description": "show activity instance in operational flowchart", "default": false }, "is_reviewed": { @@ -160867,7 +160489,7 @@ "show_activity_instance_in_protocol_flowchart": { "type": "boolean", "title": "Show Activity Instance In Protocol Flowchart", - "description": "show activity instance in operational flow chart", + "description": "show activity instance in operational flowchart", "default": false }, "keep_old_version": { @@ -161187,6 +160809,18 @@ "title": "Short Name", "description": "short name for the study arm" }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "label for the study arm" + }, "code": { "anyOf": [ { @@ -161426,6 +161060,18 @@ "title": "Short Name", "description": "short name for the study arm" }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "label for the study arm" + }, "code": { "anyOf": [ { @@ -161532,6 +161178,18 @@ "title": "Short Name", "description": "short name for the study arm" }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "label for the study arm" + }, "code": { "anyOf": [ { @@ -161630,6 +161288,18 @@ "title": "Short Name", "description": "short name for the study arm" }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "label for the study arm" + }, "code": { "anyOf": [ { @@ -161812,6 +161482,18 @@ "title": "Short Name", "description": "short name for the study arm" }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "label for the study arm" + }, "code": { "anyOf": [ { @@ -162056,6 +161738,18 @@ "title": "Short Name", "description": "short name for the study arm" }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Label", + "description": "label for the study arm" + }, "code": { "anyOf": [ { @@ -168085,7 +167779,7 @@ "show_soa_group_in_protocol_flowchart": { "type": "boolean", "title": "Show Soa Group In Protocol Flowchart", - "description": "show soa group in protocol flow chart", + "description": "show soa group in protocol flowchart", "default": false }, "study_uid": { @@ -168169,7 +167863,7 @@ "show_soa_group_in_protocol_flowchart": { "type": "boolean", "title": "Show Soa Group In Protocol Flowchart", - "description": "show soa group in protocol flow chart", + "description": "show soa group in protocol flowchart", "default": false } }, @@ -169722,19 +169416,17 @@ "nullable": true }, "visit_class": { - "type": "string", - "title": "Visit Class" + "$ref": "#/components/schemas/VisitClass" }, "visit_subclass": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/VisitSubclass" }, { "type": "null" } ], - "title": "Visit Subclass", "nullable": true }, "is_global_anchor_visit": { @@ -170088,19 +169780,17 @@ "title": "Epoch Allocation Uid" }, "visit_class": { - "type": "string", - "title": "Visit Class" + "$ref": "#/components/schemas/VisitClass" }, "visit_subclass": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/VisitSubclass" }, { "type": "null" } - ], - "title": "Visit Subclass" + ] }, "is_global_anchor_visit": { "type": "boolean", @@ -170337,19 +170027,24 @@ "title": "Epoch Allocation Uid" }, "visit_class": { - "type": "string", - "title": "Visit Class" + "anyOf": [ + { + "$ref": "#/components/schemas/VisitClass" + }, + { + "type": "null" + } + ] }, "visit_subclass": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/VisitSubclass" }, { "type": "null" } - ], - "title": "Visit Subclass" + ] }, "is_global_anchor_visit": { "type": "boolean", @@ -170426,7 +170121,6 @@ "visit_type_uid", "show_visit", "visit_contact_mode_uid", - "visit_class", "is_global_anchor_visit" ], "title": "StudyVisitEditInput" @@ -171182,19 +170876,17 @@ "nullable": true }, "visit_class": { - "type": "string", - "title": "Visit Class" + "$ref": "#/components/schemas/VisitClass" }, "visit_subclass": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/VisitSubclass" }, { "type": "null" } ], - "title": "Visit Subclass", "nullable": true }, "is_global_anchor_visit": { @@ -174090,6 +173782,19 @@ "type": "object", "title": "UserInfoPatchInput" }, + "ValidCodelistMappingInput": { + "properties": { + "valid_codelist_uids": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Valid Codelist Uids" + } + }, + "type": "object", + "title": "ValidCodelistMappingInput" + }, "ValidationDetail": { "properties": { "error_code": { @@ -174457,6 +174162,17 @@ "type": "object", "title": "VersionInfo" }, + "VisitClass": { + "type": "string", + "enum": [ + "SINGLE_VISIT", + "SPECIAL_VISIT", + "NON_VISIT", + "UNSCHEDULED_VISIT", + "MANUALLY_DEFINED_VISIT" + ], + "title": "VisitClass" + }, "VisitConsecutiveGroupInput": { "properties": { "visits_to_assign": { @@ -174707,6 +174423,16 @@ "name" ], "title": "VisitNamePostInput" + }, + "VisitSubclass": { + "type": "string", + "enum": [ + "SINGLE_VISIT", + "ADDITIONAL_SUBVISIT_IN_A_GROUP_OF_SUBV", + "ANCHOR_VISIT_IN_GROUP_OF_SUBV", + "REPEATING_VISIT" + ], + "title": "VisitSubclass" } }, "securitySchemes": { diff --git a/clinical-mdr-api/pyproject.toml b/clinical-mdr-api/pyproject.toml index 9aa9e9f8..18fe2406 100644 --- a/clinical-mdr-api/pyproject.toml +++ b/clinical-mdr-api/pyproject.toml @@ -19,7 +19,7 @@ addopts = "--cov-config=.coveragerc" [tool.isort] profile = 'black' -src_paths = ['clinical_mdr_api', 'consumer_api', 'common', 'sblint'] +src_paths = ['clinical_mdr_api', 'consumer_api', 'common', 'sblint', 'extensions'] [tool.pylint.'MASTER'] extension-pkg-allow-list = 'pydantic, lxml' diff --git a/clinical-mdr-api/templates/odm/crf.html b/clinical-mdr-api/templates/odm/crf.html new file mode 100644 index 00000000..0a0da9fe --- /dev/null +++ b/clinical-mdr-api/templates/odm/crf.html @@ -0,0 +1,810 @@ + + + + + + Case Report Form + + + + +
+
+

Case Report Form (CRF)

+
+ + + + + + +
+
+ + {% macro render_item(item_in, item_group_uid, form_uid, group_domains) %} + {% set item = data.odm_items | selectattr('uid', '==', item_in.uid) | first | default('') %} + {% if item_in.vendor %} + {% set indent_attr = item_in.vendor.attributes | selectattr('name', '==', 'indentLevel') | first | default('') + %} + {% set indent_level = ((indent_attr.value)|int * 2)|string if indent_attr else '' %} + {% else %} + {% set indent_level = '' %} + {% endif %} + + {% if item.datatype|lower == 'comment' %} + +

+ + + + {{ item.name }} +

+ + {% else %} + + {{ '*' if item_in.mandatory|lower == 'yes' else '' }} + + +
+

+ + + + {{ item.name }} +

+ + + {% if item_group_uid and form_uid %} + {% set item_activity_instance = item.activity_instances | selectattr('odm_item_group_uid', + '==', item_group_uid) | selectattr('odm_form_uid', '==', form_uid) | first | default('') %} + {% if item_activity_instance %} + + {% endif %} + {% endif %} + + {% set item_activity_instances = item.vendor_elements | selectattr('name', '==', 'ActivityInstance') + %} + {% for item_activity_instance in item_activity_instances %} + {% set topic_code = item.vendor_element_attributes | selectattr('vendor_element_uid', '==', + item_activity_instance.uid) | selectattr('name', '==', 'topicCode') | first | default('') %} + {% set adam_param_code = item.vendor_element_attributes | selectattr('vendor_element_uid', '==', + item_activity_instance.uid) | selectattr('name', '==', 'adamCode') | first | default('') %} + + {% endfor %} + + +
+ + +
+ {% if item.codelist is not none %} + {% for term in item.terms %} +
+ + + +
+ + {% endfor %} + + {% else %} + + {% endif %} + + {% if item.length is not none and not item.terms %} + + {{ item.length }} char(s) + + {% endif %} + + + + {{ render_units(item.unit_definitions, item.uid, item_group_uid, form_uid) }} +
+ + {% endif %} + + + {% if item.sds_var_name and ':' in item.sds_var_name %} + {% for domain in item.sds_var_name.split('|') %} + + {% endfor %} + {% elif item.sds_var_name and ',' in item.sds_var_name %} + {% for domain in item.sds_var_name.split(',') %} + + {% endfor %} + {% elif item.sds_var_name %} + + {% endif %} + + {% for alias in item.aliases %} + + {% endfor %} + + + {% endmacro %} + + {% macro render_item_group(item_group_in, form_uid) %} + {% set item_group = data.odm_item_groups | selectattr('uid', '==', item_group_in.uid) | first | default('') %} + {% set domains = {} %} + {% for domain in item_group.sdtm_domains %} + {% set _ = domains.update({domain.submission_value: (domain.term_name, domain_color(loop.index0))}) %} + {% endfor %} + + + + + + + + + + {% for item_ref in item_group.items %} + {{ render_item(item_ref, item_group.uid, form_uid, domains) }} + {% endfor %} + +
+

+ + + + {{ item_group.name }} + + {{'*' if item_group_in.mandatory|lower == 'yes' else '' }} + +

+ + +
+ {% for sub_val, (name, color) in domains.items() %} + + {% endfor %} + +
+ {% endmacro %} + + {% macro render_form(form_in) %} +
+
+

+ + + + {{ form_in.name }} +

+ + +
+ + {% for item_group_ref in form_in.item_groups %} + {{ render_item_group(item_group_ref, form_in.uid) }} + {% endfor %} +
+ {% endmacro %} + + + {% macro render_units(units, item_uid, item_group_uid, form_uid) %} + {% if units and units|length > 0 %} + + + + + +
Units: + {% for unit in units %} +
+ + +
+ + {% endfor %} +
+ {% endif %} + {% endmacro %} + + {% macro render_all_forms(forms) %} + {% for form in forms %} + {{ render_form(form) }} + {% endfor %} + {% endmacro %} + + {% macro render_all_item_groups(item_groups) %} + {% for item_group in item_groups %} + {{ render_item_group(item_group, none) }} + {% endfor %} + {% endmacro %} + + {% macro render_all_items(items) %} + {% for item in items %} + {{ render_item(item, none, none, {}) }} + {% endfor %} + {% endmacro %} + + {% macro get_input_type(type) %} + {% if type == 'integer' %} + number + {% elif type == 'date' %} + date + {% elif type == 'time' %} + time + {% elif type == 'datetime' %} + datetime-local + {% else %} + text + {% endif %} + {% endmacro %} + + {% macro domain_color(index) %} + {% set colors = ["#BFFFFF", "#FFFF96", "#96FF96", "#FFBF9C", "#3DB7E9", "#E69F00", "#F748A5"] %} + {{ colors[index % colors|length] }} + {% endmacro %} + + {% if data.odm_forms and data.odm_forms|length > 0 %} + {{ render_all_forms(data.odm_forms) }} + {% elif data.odm_item_groups and data.odm_item_groups|length > 0 %} + {{ render_all_item_groups(data.odm_item_groups) }} + {% elif data.odm_items and data.odm_items|length > 0 %} + {{ render_all_items(data.odm_items) }} + {% endif %} +
+ + + + + \ No newline at end of file diff --git a/clinical-mdr-api/xml_stylesheets/blank.xsl b/clinical-mdr-api/xml_stylesheets/blank.xsl deleted file mode 100644 index 1dcab63e..00000000 --- a/clinical-mdr-api/xml_stylesheets/blank.xsl +++ /dev/null @@ -1,546 +0,0 @@ - - - - - - - - <xsl:value-of select="/ODM/Study/@OID"/> - - - - - - - -
-
-
-

-
-
- -
-
-
- - - -
-
- Black labels are Mandatory (otherwise Green) -
-
- lock Lock -
-
- * Data Entry Required -
-
- account_tree Source Data Verification (SDV) -
-
- - - - - - - -
- - - - - - - - checkbox - checkbox - radio - - - - - - blackItem - greenItem - greenItem - - - - - -
- - row greenItem - - - row blackItem - - - - -
-
- - - - - - - - - - - - -
- - -
- - * - - - lock - - - account_tree - -
-
- - - - - - - - - - - - - - - - - - -
- - - - -
- - - -    - - -
-
- Unit : -
-
- - -   - -
-
-
-
- -
- - - - - -  
-
- - -  
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
- - -    - - -
-
-
-
- -
- - - -
-
-
-

- G   - - -  *  - -

- - - - - -
- - - - - - - - -
-
-
- - -
-
-
-
-

F  

- - - - - -
- - - - -
-
-
- - - - - -   - - - lock account_tree - - - lock - - - account_tree - - - - - - - - - - - - - - - - - -
-
- : -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/clinical-mdr-api/xml_stylesheets/falcon.xsl b/clinical-mdr-api/xml_stylesheets/falcon.xsl index 984fd784..6339950e 100644 --- a/clinical-mdr-api/xml_stylesheets/falcon.xsl +++ b/clinical-mdr-api/xml_stylesheets/falcon.xsl @@ -20,13 +20,13 @@ body { font-family: Arial; background-color: #ffffff; - margin: 10px; + margin: 0px; } h1, h2, h3, h4, h5 { - margin-top: 3px; - margin-bottom: 3px; - padding: 5px; + margin-top: 2px; + margin-bottom: 1px; + padding: 3px; } .btn { @@ -40,42 +40,48 @@ color: #fff !important; } - .oidinfo { - color: red !important; + em { + font-weight: bold; font-style: normal; - font-size: 12px; + color: #f00; } .badge { display: inline-block; margin: 0.2em; - padding: 0.25em 0.4em; - font-size: 80%; + padding: 2px 4px; + font-size: 70%; font-weight: 550; line-height: 1; text-align: center; width: auto; white-space: normal; vertical-align: baseline; - border-radius: 0px; + border-radius: 5px; transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; } .badge-ig { display: inline-block; margin: 0.2em; - padding: 0.25em 0.4em; - font-size: 90%; + padding: 6px 8px; + font-size: 80%; font-weight: 600; line-height: 1; text-align: center; width: auto; white-space: normal; vertical-align: baseline; - border-radius: 0px; + border-radius: 8px; transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; } + .oidinfo { + color: red !important; + font-style: normal; + font-size: 12px; + } + @media print { .page-break { page-break-before: always; @@ -91,229 +97,262 @@ div.Section2 {page:Section2;} [if gte mso 9]> - <style> + <style> .d-print-none { - mso-hide: all; - display: none !important; - visibility: hidden !important; - height: 0 !important; - width: 0 !important; - overflow: hidden !important; - font-size: 0 !important; - line-height: 0 !important; - margin: 0 !important; - padding: 0 !important; + mso-hide: all; + display: none !important; + visibility: hidden !important; + height: 0 !important; + width: 0 !important; + overflow: hidden !important; + font-size: 0 !important; + line-height: 0 !important; + margin: 0 !important; + padding: 0 !important; } - </style> - <![endif] - - -
- - - [if !mso]><! -
-   -   -   -   - -
- <![endif] - - -
-
- - - - - - - + </style> + <![endif] + + +
+ + + [if !mso]><! +
+   +   +   + +
+ <![endif] + + +
+
+ + + + + + + - - + + - - - - - - - - - - + + + + + + + + + + - - - - -

- -

- -
- - - -  *  - - - - - - - - - - - - - - - - - - - [if !mso]><! + + + + + + +

+ +

+ +
+ + + + +  *  + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [if !mso]><!
[OID=, Version=]
- <![endif] - - - - - -
-    -
- [if !mso]><! + <![endif] + + + + + +
+ +    + + + + + + + + + +
+ [if !mso]><!
[OID=, Version=]
- <![endif] - -
- - -
-    -
- [if !mso]><! + <![endif] +
+ + +
+ +    + + + + + + + + + +
+ [if !mso]><!
[OID=, Version=]
+ <![endif] +
+ [if !mso]><! +
[OID=, Version=]
<![endif] -
- [if !mso]><! -
[OID=, Version=]
- <![endif] - -
- -   - - - -
- - - - - - - - - - - - - + +   - + + + + + + + +
- - + + + + + + + + + + + + + + + + + + +
+ + + + -
- - - - + +
digit(s)
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - -
digit(s)
+
+
+
+ + +

+ + +  *  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

- - -  *  - -

- [if !mso]><! -
[OID=, Version=]
+

+ [if !mso]><! +
[OID=, Version=]
<![endif] - +

@@ -389,17 +426,17 @@ - - + + - + Study ID: NNXXXX-XXXX - + Integration @@ -409,7 +446,7 @@ select="//ItemGroupDef[@OID = current()/@ItemGroupOID]" /> - + @@ -452,148 +489,190 @@

- - - - - - + + + + - - + + - + -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + +
+ Activity Instance  +
+ + +
+ +
+ Topic Code: +
+ + +
+ Param Code: +
+
- +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
- \ No newline at end of file + \ No newline at end of file diff --git a/clinical-mdr-api/xml_stylesheets/with-annotations.xsl b/clinical-mdr-api/xml_stylesheets/with-annotations.xsl index 3fc58bf9..553a16a3 100644 --- a/clinical-mdr-api/xml_stylesheets/with-annotations.xsl +++ b/clinical-mdr-api/xml_stylesheets/with-annotations.xsl @@ -33,7 +33,7 @@ } .btn-clicked { - background-color: #BDD6EE !important; /* Example: a blue color */ + background-color: #BDD6EE !important; border-color: #BDD6EE !important; color: #000 !important; } @@ -43,18 +43,18 @@ top: 0; left: 0; width: 99%; - height: 50px; - padding: 5px; + height: 0px; + padding: 2px; background-color: #ffffff; color: black; - line-height: 20px; + line-height: 14px; font-size: 12px; z-index: 1000; } .content { - margin-top: 50px; /* pushes content below the fixed bar */ - padding: 10px; + margin-top: 40px; /* pushes content below the fixed bar */ + padding: 5px; } .content .odmForm { @@ -64,24 +64,6 @@ border-radius: 15px; /* Rounded border for the form */ } - .odmform { - background-color: #fff; - padding: 10px; - max-width: 99%; - margin: auto; - border-radius: 15px; /* Rounded border for the form */ - padding-top: 80px; - } - - .odmform + .odmform { - margin-top: 20px; /* space between divs */ - } - - .odmform h2 { - margin-top: 0; - margin-bottom: 20px; - } - .odmitemgroup { border-radius: 10px; padding: 15px; @@ -137,8 +119,9 @@ margin-top: 0.2em; margin-bottom: 0.2rem; border: 1px solid #0000001c; - border-radius: 0.2rem; + border-radius: 2.2rem; font-style: italic; + width: fit-content; } .alert-secondary { @@ -186,33 +169,44 @@ .badge { display: inline-block; margin: 0.2em; - padding: 0.25em 0.4em; - font-size: 80%; + padding: 2px 4px; + font-size: 70%; font-weight: 550; line-height: 1; text-align: center; width: auto; white-space: normal; vertical-align: baseline; - border-radius: 0px; - transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s - ease-in-out,box-shadow .15s ease-in-out; + border-radius: 5px; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; } .badge-ig { display: inline-block; margin: 0.2em; - padding: 0.25em 0.4em; - font-size: 90%; + padding: 6px 8px; + font-size: 80%; font-weight: 600; line-height: 1; text-align: center; width: auto; white-space: normal; vertical-align: baseline; - border-radius: 0px; - transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s - ease-in-out,box-shadow .15s ease-in-out; + border-radius: 8px; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; + } + + .cdash-container, .designNotes-container { + display: flex; + } + + .alert.sponsor p, .alert.help p { + display: inline; + margin: 0; + } + + p { + margin: 10px 0px 10px 0px; } split-item { @@ -220,9 +214,6 @@ width: 100%; } - split-item .badge .badge-ig { - } - @media print { .page-break { page-break-before: always; /* For older browsers */ @@ -242,12 +233,11 @@
-     -   +   +   +     -   -  
@@ -294,8 +284,7 @@
- + blackItem greenItem @@ -304,6 +293,7 @@ + @@ -317,6 +307,7 @@
+ row greenItem @@ -370,6 +361,7 @@
+
* @@ -381,7 +373,13 @@ account_tree
-
+
+ + @@ -397,149 +395,165 @@
[OID=, Version=]
- - - - - - - - - - - - + + + + + + + -
- - - -
- - -   digit(s) + + + + + + + +
+ + + + +
+ + +   digit(s) + + +   digit(s) + + + + + - -   digit(s) - - - - - - - - -    - - -
-
- Unit:  + + + + + + + +    + + +
+
+ Unit:  +
+
+ +   +   [OID=, + Version= + ] +
+
- -   -   [OID=, - Version= - ] -
-
-
-
+ class="col-sm-2 {$labelColor} border text-center"> + + + + + + + - - + + - - - - - - - - - - - - - - - - -
-
- - - - -
+ + + + + + + + + +
+
+ + + + +
+ -   [
- [OID=, Version= - -
- - -   
- [OID=] -
- [OID=, Version=] - - - - - - - - - - - - - - - - - - -
+
@@ -567,451 +581,491 @@
- - - - - - - -   digit(s) + + + + + + + + +   + + digit(s) + -   digit(s) - - +   + + digit(s) + + + + + +   digit(s) - +   digit(s) - - -   digit(s) - - - -    - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - + aria-describedby="item{@OID}" />   digit(s) + + + +    -
-
- + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
-
- + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - +
+
+
+ +

+ G   + +  *  + +

+
+ [OID=, Version=] +
- - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - -
-
-
-

- G   - -  *  - -

-
- [OID=, Version=] + + + - - - - - - - - - - - - - - - -
-
- -

- - - - - - -

-
- -
- - - - - - - - -
-
-
-
- - - - - - - - - + +
- - - -
-
-
-
-

F  

- [OID=, Version=] - - - - - - - - - - -
- - - - - -
+
+ +

+ + + + + + +

+
+ +
+ + + + + + + + +
+
- - - - - - - - -   - - lock account_tree - - - lock - - - account_tree +
+ + + + + + + + + +
+ + + +
+
+
+
+

F  

+ [OID=, Version=] + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+ + + + + + + + + + + lock account_tree + + + lock + + + account_tree + + + + + + + + + + + + + + + + + + + + + - + + + - - - - - - - - - - - - - - -
-
- + +
+
+ +
+ Activity Instance  +
+ +
+ +
+ Topic Code: +
+ +
+ Param Code: +
- - - - - - - - - - - - - - - - - -
- - - - - - - -
-
- - - -
-
-
-
- - - - - - -
- - - - - - -
-
- - - -
-
- - - -
-
- -
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - -   - - - - - -   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+ + + + + +
+
+
- +
+
- - - - + + + + + + + + + + + + + + + +
- - - - - - - + + + + + + +
+
+ + + +
+
+
+
+ + + + + + +
+ + + + + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + - +
+
- \ No newline at end of file + \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 9b0f4fe6..4def931a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -88,6 +88,29 @@ services: UVICORN_ROOT_PATH: "/consumer-api" UVICORN_APP: consumer_api.consumer_api:app + # Extensions API image & service + extensionsapi: + build: + context: ./clinical-mdr-api + dockerfile: Dockerfile + args: + TARGET: ${BUILD_TARGET:-dev} + image: ${API_IMAGE:-} + depends_on: + database: + condition: service_healthy + environment: + NEO4J_DSN: "${NEO4J_DSN:-bolt://neo4j:changeme1234@database:7687/mdrdb}" + ALLOW_ORIGIN_REGEX: "${ALLOW_ORIGIN_REGEX:-.*}" + OAUTH_ENABLED: "${OAUTH_ENABLED:-False}" + OAUTH_RBAC_ENABLED: "${OAUTH_RBAC_ENABLED:-False}" + OAUTH_METADATA_URL: "${OAUTH_METADATA_URL:-}" + OAUTH_API_APP_ID: "${OAUTH_API_APP_ID:-}" + OAUTH_SWAGGER_APP_ID: "${OAUTH_SWAGGER_APP_ID:-}" + UVICORN_PORT: 5009 + UVICORN_ROOT_PATH: "/extensions-api" + UVICORN_APP: extensions.extensions_api:app + # Frontend image for production (see UI service for local development) frontend: build: @@ -105,6 +128,10 @@ services: condition: service_healthy neodash: condition: service_healthy + consumerapi: + condition: service_healthy + extensionsapi: + condition: service_healthy ports: - "${BIND_ADDRESS:-127.0.0.1}:${FRONTEND_PORT:-5005}:5005" environment: diff --git a/db-schema-migration/.pre-commit-config.yaml b/db-schema-migration/.pre-commit-config.yaml new file mode 100644 index 00000000..eed3e19e --- /dev/null +++ b/db-schema-migration/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: + - repo: https://github.com/psf/black + rev: 24.10.0 + hooks: + - id: black + - repo: https://github.com/PyCQA/isort + rev: 5.13.2 + hooks: + - id: isort + - 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/db-schema-migration/Pipfile b/db-schema-migration/Pipfile index 6172a57d..21f4ca20 100644 --- a/db-schema-migration/Pipfile +++ b/db-schema-migration/Pipfile @@ -30,12 +30,12 @@ python_version = "3.13" [scripts] build-sbom = "pipenv run python3 assbom.py --pipfile --fallback-dir doc/licenses --level 2 --output sbom.md" -test = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml tests/test_migration_018.py" -verify = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml verifications/verification_018.py" -migrate = "python -m migrations.migration_018" -test_corrections = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml tests/test_correction_016.py" -verify_corrections = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml verifications/correction_verification_016.py" -apply_corrections = "python -m data_corrections.correction_016" +test = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml tests/test_migration_019.py" +verify = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml verifications/verification_019.py" +migrate = "python -m migrations.migration_019" +test_corrections = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml tests/test_correction_017.py" +verify_corrections = "python -m pytest -s --cov-report html:test_coverage --cov-report xml:reports/coverage.xml --cov-append --cov --junitxml=reports/test_report.xml verifications/correction_verification_017.py" +apply_corrections = "python -m data_corrections.correction_017" wipe_single_study = "python -m data_corrections.correction_wipe_study" token = "python -m migrations.auth" lint = "pylint migrations tests verifications data_corrections" diff --git a/db-schema-migration/README.md b/db-schema-migration/README.md index b71d9080..3ec94523 100644 --- a/db-schema-migration/README.md +++ b/db-schema-migration/README.md @@ -296,7 +296,7 @@ for running locally. Store the token in the `WIKI_PERSONAL_ACCESS_TOKEN` environment variable to use this method. If given, this token is then used with the "Basic" authentication type. -For running in pipelies, instead give a system token in the `WIKI_API_TOKEN` enviromnent variable. +For running in pipelines, instead give a system token in the `WIKI_API_TOKEN` enviromnent variable. If given, this token is then used with the "Bearer" authentication type. Run the script, providing the markdown file as an argument: diff --git a/db-schema-migration/data_corrections/correction_016.py b/db-schema-migration/data_corrections/correction_016.py index b4dfd444..f1047bf9 100644 --- a/db-schema-migration/data_corrections/correction_016.py +++ b/db-schema-migration/data_corrections/correction_016.py @@ -438,7 +438,9 @@ def solve_not_latest_has_version_lacks_end_date(db_driver, log, run_label): return counters.contains_updates -@capture_changes(verify_func=correction_verification_016.test_remove_isolated_orphan_nodes) +@capture_changes( + verify_func=correction_verification_016.test_remove_isolated_orphan_nodes +) def remove_isolated_orphan_nodes(db_driver, log, run_label): """ ## Remove isolated orphan nodes (Bug #3473052) diff --git a/db-schema-migration/data_corrections/correction_017.py b/db-schema-migration/data_corrections/correction_017.py new file mode 100644 index 00000000..0472cba7 --- /dev/null +++ b/db-schema-migration/data_corrections/correction_017.py @@ -0,0 +1,407 @@ +"""PRD Data Corrections: Before Release 2.5""" + +import os + +from data_corrections.utils.utils import ( + capture_changes, + get_db_driver, + print_counters_table, + run_cypher_query, + save_md_title, +) +from migrations.utils.utils import api_delete, api_get, api_post, get_logger +from verifications import correction_verification_017 + +LOGGER = get_logger(os.path.basename(__file__)) +DB_DRIVER = get_db_driver() +CORRECTION_DESC = "data-correction-release-2.5" + + +def main(run_label="correction"): + desc = f"Running data corrections on DB '{os.environ['DATABASE_NAME']}'" + LOGGER.info(desc) + save_md_title(run_label, __doc__, desc) + + remove_duplicated_non_visit_and_unscheduled_visits(DB_DRIVER, LOGGER, run_label) + fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name( + DB_DRIVER, LOGGER, run_label + ) + rebuild_missing_protocol_soa_snapshots(DB_DRIVER, LOGGER, run_label) + remove_soa_cell_relationships_without_released_study(DB_DRIVER, LOGGER, run_label) + remove_study_action_with_broken_after(DB_DRIVER, LOGGER, run_label) + fix_not_coherent_in_time_library_selection(DB_DRIVER, LOGGER, run_label) + fix_studies_different_versions_with_the_same_start_date( + DB_DRIVER, LOGGER, run_label + ) + + +@capture_changes( + verify_func=correction_verification_017.test_remove_duplicated_non_visit_and_unscheduled_visits +) +def remove_duplicated_non_visit_and_unscheduled_visits(db_driver, log, run_label): + """ + ### Problem description + There should only exist one Non-visit and Unscheduled-visit in a given Study. + There was an API issue that when both Non-visit and Unscheduled-visit existed in given Study and we were editing + Non-visit to be Unscheduled-visit or vice versa, the API allowed to created duplicated Non or Unscheduled visit by edition. + ### Change description + - Delete duplicated Non-visit or Unscheduled-visit in the Study. + ### Nodes and relationships affected + - `StudyVisit` node + ### Expected changes: 1 call for DELETE /study-visits/{StudyVisit_000196} to delete duplicated non-visit. + """ + contains_updates = [] + query = """ + MATCH (sr:StudyRoot)--(sv:StudyValue)-[:HAS_STUDY_VISIT]->(study_visit:StudyVisit) + WHERE NOT (study_visit)-[:BEFORE]-() AND study_visit.visit_class = "NON_VISIT" + WITH DISTINCT sr, collect(distinct study_visit.uid) as study_visit_uids + WHERE size(study_visit_uids) > 1 + RETURN sr.uid as study_uid, study_visit_uids + """ + results, _ = run_cypher_query(db_driver, query) + for result in results: + study_uid = result[0] + non_visit_uids = result[1] + log.info( + f"Run: {run_label}, Removing duplicated Non visits in Study {study_uid}" + ) + for non_visit_uid in non_visit_uids[1:]: + response = api_delete(f"/studies/{study_uid}/study-visits/{non_visit_uid}") + contains_updates.append(response) + query = """ + MATCH (sr:StudyRoot)--(sv:StudyValue)-[:HAS_STUDY_VISIT]->(study_visit:StudyVisit) + WHERE NOT (study_visit)-[:BEFORE]-() AND study_visit.visit_class = "UNSCHEDULED_VISIT" + WITH DISTINCT sr, collect(distinct study_visit.uid) as study_visit_uids + WHERE size(study_visit_uids) > 1 + RETURN sr.uid as study_uid, study_visit_uids + """ + results, _ = run_cypher_query(db_driver, query) + for result in results: + study_uid = result[0] + unscheduled_visit_uids = result[1] + log.info( + f"Run: {run_label}, Removing duplicated Unscheduled visits in Study {study_uid}" + ) + for unscheduled_visit_uid in unscheduled_visit_uids[1:]: + response = api_delete( + f"/studies/{study_uid}/study-visits/{unscheduled_visit_uid}" + ) + contains_updates.append(response) + return any(contains_updates) + + +@capture_changes( + verify_func=correction_verification_017.test_fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name +) +def fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name( + db_driver, log, run_label +): + """ + ### Problem description + There exists an issue that some different StudyVisits share the same unique visit number. + It should not be the case as unique visit number should be a unique value across all StudyVisits within a Study. + The issue existed only for groups of subvisits when the anchor visit timing was edited and it was not properly updated. + ### Change description + - Update unique visit number and other properties (visit_name, short_visit_label, visit_number) for StudyVisits having wrong values + - Correct values are taken from API that calculates them based on the StudyVisit full schedule. + ### Nodes and relationships affected + - `StudyVisit` node + ### Expected changes: 2 StudyVisits (StudyVisit_004596, StudyVisit_009676) updated by correcting (visit_number, unique_visit_number, short_visit_label) properties and relationship to dependent visit_name node + """ + contains_updates = [] + query = """ + MATCH (sr:StudyRoot)--(sv:StudyValue)-[:HAS_STUDY_VISIT]->(study_visit:StudyVisit) + WHERE NOT (study_visit)-[:BEFORE]-() + WITH DISTINCT sr, study_visit.unique_visit_number as unique_visit_number, collect(distinct study_visit.uid) as study_visit_uids + WHERE size(study_visit_uids) > 1 + RETURN sr.uid as study_uid, study_visit_uids, unique_visit_number + """ + results, _ = run_cypher_query(db_driver, query) + for result in results: + study_uid = result[0] + study_visit_uids = result[1] + unique_visit_number = result[2] + for study_visit_uid in study_visit_uids: + study_visit = api_get( + f"/studies/{study_uid}/study-visits/{study_visit_uid}", + params={"derive_props_based_on_timeline": True}, + ).json() + correct_study_visit_number = study_visit["unique_visit_number"] + correct_short_visit_label = study_visit["visit_short_name"] + correct_visit_number = study_visit["visit_number"] + correct_visit_name = study_visit["visit_name"] + if correct_study_visit_number != int(unique_visit_number): + visit_name, _ = run_cypher_query( + db_driver, + """ + MATCH (visit_name_root:VisitNameRoot)-[:LATEST]->(visit_name_value:VisitNameValue {name: $value}) + RETURN visit_name_root.uid + """, + params={ + "value": correct_visit_name, + }, + ) + if visit_name: + visit_name_uid = visit_name[0][0] + else: + visit_name_uid = api_post( + "/concepts/visit-names", + payload={ + "name": correct_visit_name, + "template_parameter": True, + "library_name": "Sponsor", + }, + ).json()["uid"] + log.info( + f"Run: {run_label}, Modifying unique visit number for {study_visit_uid} from {unique_visit_number} to {correct_study_visit_number}" + ) + query = """ + MATCH (study_visit:StudyVisit {uid: $study_visit_uid})-[existing_visit_name_rel:HAS_VISIT_NAME]->(visit_name_root:VisitNameRoot) + WHERE NOT (study_visit)-[:BEFORE]-() + SET study_visit.unique_visit_number = $unique_visit_number + SET study_visit.short_visit_label = $short_visit_label + SET study_visit.visit_number = $visit_number + WITH study_visit, existing_visit_name_rel + MATCH (new_visit_name_root:VisitNameRoot {uid: $visit_name_uid}) + CREATE (study_visit)-[:HAS_VISIT_NAME]->(new_visit_name_root) + DETACH DELETE existing_visit_name_rel + """ + _, summary = run_cypher_query( + db_driver, + query, + params={ + "study_visit_uid": study_visit_uid, + "unique_visit_number": correct_study_visit_number, + "short_visit_label": correct_short_visit_label, + "visit_number": correct_visit_number, + "visit_name_uid": visit_name_uid, + }, + ) + counters = summary.counters + print_counters_table(counters) + contains_updates.append(counters.contains_updates) + return any(contains_updates) + + +@capture_changes( + verify_func=correction_verification_017.test_protocol_soa_response_status_code +) +def rebuild_missing_protocol_soa_snapshots(db_driver, log, run_label): + """ + ### Problem description + Some (RELEASED) Study versions do not have a Protocol SoA snapshot created due to a (fixed) bug. + ### Change description + - Rebuild missing/failing Protocol SoA snapshots for RELEASED Study versions, using contemporary StudySelections but latest ordering. + ### Relationships affected + - (StudyValue)-[:HAS_PROTOCOL_SOA_CELL]->(StudySelection) + - (StudyValue)-[:HAS_PROTOCOL_SOA_FOOTNOTE]->(StudySelection) + ### Expected changes + - Old relationships removed + - New relationships created + """ + + # Find all RELEASED Study versions of all Studies + results, _ = run_cypher_query( + db_driver, + """ + MATCH (study_root:StudyRoot)-[study_version:HAS_VERSION {status: 'RELEASED'}]->(study_value:StudyValue) + RETURN study_root.uid, study_version.version + """, + ) + + # For all Studies and their RELEASED versions + for result in results: + study_uid, study_version = result + + # Check API endpoint returns Protocol SoA Snapshot + log.info( + "Run: %s, Checking Protocol SoA Snapshot for Study '%s' Version '%s'", + run_label, + study_uid, + study_version, + ) + response = api_get( + f"/studies/{study_uid}/flowchart/snapshot", + params={"study_value_version": study_version, "layout": "protocol"}, + check_ok_status=False, + ) + log.info( + "Run: %s, Protocol SoA Snapshot response status code %i for Study '%s' Version '%s'", + run_label, + response.status_code, + study_uid, + study_version, + ) + + if response.status_code != 200: + # Rebuild Protocol SoA Snapshot + log.info( + "Run: %s, Rebuilding Protocol SoA Snapshot for Study '%s' Version '%s'", + run_label, + study_uid, + study_version, + ) + response = api_post( + f"/studies/{study_uid}/flowchart/snapshot", + payload={}, + params={"study_value_version": study_version, "layout": "protocol"}, + ) + + +@capture_changes( + verify_func=correction_verification_017.test_remove_study_action_with_broken_after +) +def remove_study_action_with_broken_after(db_driver, log, run_label): + """ + ### Problem description + Some StudyAction nodes exist without AFTER relationships. These are leftover from migrated study selections + where the StudySelection nodes were deleted but the StudyAction nodes remained. Every StudyAction (except + UpdateSoASnapshot) must have an AFTER relationship. + ### Change description + - Delete StudyAction nodes that don't have AFTER relationships (excluding UpdateSoASnapshot nodes). + ### Nodes and relationships affected + - `StudyAction` nodes + """ + log.info( + f"Run: {run_label}, Removing StudyAction nodes without AFTER relationships" + ) + + query = """ + MATCH (sr:StudyRoot)-[:AUDIT_TRAIL]->(sa:StudyAction) + WHERE + NOT (sa)-[:AFTER]->() + AND NOT sa:Delete + AND NOT (sa)-[:BEFORE]->(:StudyValue) + AND NOT sa:UpdateSoASnapshot + DETACH DELETE sa + """ + + _, summary = run_cypher_query(db_driver, query) + counters = summary.counters + print_counters_table(counters) + return counters.contains_updates + + +@capture_changes( + verify_func=correction_verification_017.test_fix_not_coherent_in_time_library_selection +) +def fix_not_coherent_in_time_library_selection(db_driver, log, run_label): + """ + ### Problem description + Some StudyActivity nodes reference ActivityValue nodes that don't have valid HAS_VERSION + relationships at the time when the Create action was performed. This happens when the + ActivityValue version's start_date or end_date doesn't cover the Create action date. + Specifically, Activity_000317 version 7.0's HAS_VERSION relationship doesn't cover + the dates when some Create actions were performed. + ### Change description + - Fix version 7.0 of Activity_000317 by adjusting its HAS_VERSION start_date to cover + the earliest Create action date that references it + ### Nodes and relationships affected + - `HAS_VERSION` relationship for Activity_000317 version 7.0 + """ + log.info( + f"Run: {run_label}, Fixing not coherent in time library selection for Activity_000317 version 7.0" + ) + + # Find Create actions that reference Activity_000317 version 7.0 where the version is not valid + # Then adjust version 7.0's start_date to be before the earliest such Create date + query = """ + MATCH (ar:ActivityRoot {uid: "Activity_000317"})-[hv:HAS_VERSION {version: "7.0"}]->(av:ActivityValue) + WHERE not hv.end_date = datetime('2024-12-17T15:18:50.527858001Z') + SET hv.end_date = datetime('2024-12-17T15:18:50.527858001Z') + RETURN count(hv) AS updated_count + """ + + _, summary = run_cypher_query(db_driver, query) + counters = summary.counters + print_counters_table(counters) + return counters.contains_updates + + +@capture_changes( + verify_func=correction_verification_017.test_fix_studies_different_versions_with_the_same_start_date +) +def fix_studies_different_versions_with_the_same_start_date(db_driver, log, run_label): + """ + ### Problem description + Some StudyRoot nodes have different versions with the same start_date, which violates + the constraint that no version should have a start_date greater than or equal to the + latest version's start_date. This happens when a previous version has the same start_date + as the latest version. + ### Change description + - For each StudyRoot, find versions with start_date >= the latest version's start_date + - Subtract 1 millisecond from those previous versions' start_date to make them earlier + - This ensures proper chronological ordering of versions + ### Nodes and relationships affected + - `HAS_VERSION` relationships for StudyRoot nodes + """ + log.info( + f"Run: {run_label}, Fixing studies with different versions having the same start_date" + ) + + # Find StudyRoot nodes where a previous version has start_date >= latest version's start_date + # Subtract 1 millisecond from the previous version's start_date + query = """ + MATCH (root:StudyRoot)-[:LATEST]->(latest) + MATCH (root)-[v_latest:HAS_VERSION|LATEST_DRAFT|LATEST_FINAL|LATEST_LOCKED|LATEST_RETIRED|LATEST_RELEASED]->(latest) + WITH root, latest, v_latest.start_date as latest_start_date + MATCH (root)-[v_prev:HAS_VERSION|LATEST_DRAFT|LATEST_FINAL|LATEST_LOCKED|LATEST_RETIRED|LATEST_RELEASED]->(prev_value) + WHERE prev_value <> latest + AND v_prev.start_date >= latest_start_date + AND v_prev.start_date IS NOT NULL + SET v_prev.start_date = v_prev.start_date - duration({milliseconds: 1}) + RETURN count(v_prev) AS updated_count + """ + + _, summary = run_cypher_query(db_driver, query) + counters = summary.counters + print_counters_table(counters) + return counters.contains_updates + + +@capture_changes( + verify_func=correction_verification_017.test_remove_soa_cell_relationships_without_released_study +) +def remove_soa_cell_relationships_without_released_study(db_driver, log, run_label): + """ + ### Problem description + Some StudyValue nodes have HAS_PROTOCOL_SOA_CELL or HAS_PROTOCOL_SOA_FOOTNOTE relationships + when the StudyRoot doesn't have a RELEASED or LOCKED version. These relationships were created + as a result of bad cloning and should not exist without a released or locked version. + ### Change description + - Delete HAS_PROTOCOL_SOA_CELL and HAS_PROTOCOL_SOA_FOOTNOTE relationships from StudyValue nodes + where the StudyRoot doesn't have a RELEASED or LOCKED version + ### Nodes and relationships affected + - `HAS_PROTOCOL_SOA_CELL` relationships + - `HAS_PROTOCOL_SOA_FOOTNOTE` relationships + """ + log.info( + f"Run: {run_label}, Removing SOA cell/footnote relationships from studies without RELEASED or LOCKED versions" + ) + + query = """ + MATCH + (sv:StudyValue)-[sv_ss]-(ss:StudySelection) + WHERE + TYPE(sv_ss)='HAS_PROTOCOL_SOA_CELL' + OR TYPE(sv_ss)='HAS_PROTOCOL_SOA_FOOTNOTE' + MATCH + (sv)-[versioning:HAS_VERSION]-(sr:StudyRoot) + WHERE NOT EXISTS( + (sr)-[:HAS_VERSION {status:'RELEASED'}]->() + ) + AND NOT EXISTS( + (sr)-[:HAS_VERSION {status:'LOCKED'}]->() + ) + DELETE sv_ss + RETURN count(sv_ss) AS deleted_count + """ + + _, summary = run_cypher_query(db_driver, query) + counters = summary.counters + print_counters_table(counters) + return counters.contains_updates + + +if __name__ == "__main__": + main() diff --git a/db-schema-migration/data_corrections/corrections_overview_017.md b/db-schema-migration/data_corrections/corrections_overview_017.md new file mode 100644 index 00000000..9165629d --- /dev/null +++ b/db-schema-migration/data_corrections/corrections_overview_017.md @@ -0,0 +1,104 @@ +## Data corrections: overview of data_corrections.correction_017 + +PRD Data Corrections: Before Release 2.5 + + + +## 1. Correction: fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name + +#### Problem description +There exists an issue that some different StudyVisits share the same unique visit number. +It should not be the case as unique visit number should be a unique value across all StudyVisits within a Study. +The issue existed only for groups of subvisits when the anchor visit timing was edited and it was not properly updated. +#### Change description +- Update unique visit number and other properties (visit_name, short_visit_label, visit_number) for StudyVisits having wrong values +- Correct values are taken from API that calculates them based on the StudyVisit full schedule. +#### Nodes and relationships affected +- `StudyVisit` node +#### Expected changes: 2 StudyVisits (StudyVisit_004596, StudyVisit_009676) updated by correcting (visit_number, unique_visit_number, short_visit_label) properties and relationship to dependent visit_name node + + +## 2. Correction: remove_duplicated_non_visit_and_unscheduled_visits + +#### Problem description +There should only exist one Non-visit and Unscheduled-visit in a given Study. +There was an API issue that when both Non-visit and Unscheduled-visit existed in given Study and we were editing +Non-visit to be Unscheduled-visit or vice versa, the API allowed to created duplicated Non or Unscheduled visit by edition. +#### Change description +- Delete duplicated Non-visit or Unscheduled-visit in the Study. +#### Nodes and relationships affected +- `StudyVisit` node +#### Expected changes: 1 call for DELETE /study-visits/{StudyVisit_000196} to delete duplicated non-visit. + + +## 3. Correction: rebuild_missing_protocol_soa_snapshots + +### Problem description +Some (RELEASED) Study versions do not have a Protocol SoA snapshot created due to a (fixed) bug. +### Change description +- Rebuild missing/failing Protocol SoA snapshots for RELEASED Study versions, using contemporary StudySelections but latest ordering. +### Relationships affected +- (StudyValue)-[:HAS_PROTOCOL_SOA_CELL]->(StudySelection) +- (StudyValue)-[:HAS_PROTOCOL_SOA_FOOTNOTE]->(StudySelection) +### Expected changes +- Old relationships removed +- New relationships created + + + +## 4. Correction: remove_study_action_with_broken_after + +#### Problem description +Some StudyAction nodes exist without AFTER relationships. These are leftover from migrated study selections +where the StudySelection nodes were deleted but the StudyAction nodes remained. Every StudyAction (except +UpdateSoASnapshot) must have an AFTER relationship. +#### Change description +- Delete StudyAction nodes that don't have AFTER relationships (excluding UpdateSoASnapshot nodes). +#### Nodes and relationships affected +- `StudyAction` nodes + + +## 5. Correction: fix_not_coherent_in_time_library_selection + +#### Problem description +Some StudyActivity nodes reference ActivityValue nodes that don't have valid HAS_VERSION +relationships at the time when the Create action was performed. This happens when the +ActivityValue version's start_date or end_date doesn't cover the Create action date. +Specifically, Activity_000317 version 7.0's HAS_VERSION relationship doesn't cover +the dates when some Create actions were performed. +#### Change description +- Fix version 7.0 of Activity_000317 by adjusting its HAS_VERSION start_date to cover + the earliest Create action date that references it +#### Nodes and relationships affected +- `HAS_VERSION` relationship for Activity_000317 version 7.0 + + +## 6. Correction: fix_studies_different_versions_with_the_same_start_date + +#### Problem description +Some StudyRoot nodes have different versions with the same start_date, which violates +the constraint that no version should have a start_date greater than or equal to the +latest version's start_date. This happens when a previous version has the same start_date +as the latest version. +#### Change description +- For each StudyRoot, find versions with start_date >= the latest version's start_date +- Subtract 1 millisecond from those previous versions' start_date to make them earlier +- This ensures proper chronological ordering of versions +#### Nodes and relationships affected +- `HAS_VERSION` relationships for StudyRoot nodes + + +## 7. Correction: remove_soa_cell_relationships_without_released_study + +#### Problem description +Some StudyValue nodes have HAS_PROTOCOL_SOA_CELL or HAS_PROTOCOL_SOA_FOOTNOTE relationships +when the StudyRoot doesn't have a RELEASED or LOCKED version. These relationships were created +as a result of bad cloning and should not exist without a released or locked version. +#### Change description +- Delete HAS_PROTOCOL_SOA_CELL and HAS_PROTOCOL_SOA_FOOTNOTE relationships from StudyValue nodes + where the StudyRoot doesn't have a RELEASED or LOCKED version +#### Nodes and relationships affected +- `HAS_PROTOCOL_SOA_CELL` relationships +- `HAS_PROTOCOL_SOA_FOOTNOTE` relationships + + diff --git a/db-schema-migration/data_corrections/utils/utils.py b/db-schema-migration/data_corrections/utils/utils.py index 0b787c85..ac92adf3 100644 --- a/db-schema-migration/data_corrections/utils/utils.py +++ b/db-schema-migration/data_corrections/utils/utils.py @@ -178,11 +178,30 @@ def get_changes_since_id(driver, change_id): CALL db.cdc.query($change_id) """ records, _ = run_cypher_query(driver, query, params={"change_id": change_id}) - return records + # Convert Neo4j Record objects to lists for JSON serialization + # Each record from CDC query contains the change data as a list of 5 elements: + # [id, tx_id, seq, tx_metadata, change_details] + # The Record object may have the data in different formats, so we handle both cases + converted_changes = [] + for record in records: + if hasattr(record, "values"): + # Record.values() returns a tuple of values + values = list(record.values()) + # If the record has a single value that is itself a list/array, use it directly + # Otherwise, use the values as-is + if len(values) == 1 and isinstance(values[0], (list, tuple)): + converted_changes.append(list(values[0])) + else: + converted_changes.append(values) + else: + # If it's already a list/tuple, convert to list + converted_changes.append(list(record)) + return converted_changes # ---------- CDC logging wrapper ---------- + # Helper to get a unique function name based on the arguments def _get_func_name(summary_params): func_name = summary_params["func_name"] @@ -220,14 +239,17 @@ def save_change_report( summary = summarize_changes(changes) summary_params["filename"] = f"{func_name}.{label}.json" # Save the full change log as a separate file + # Only write if there are actual changes, or if file doesn't exist yet + json_filepath = os.path.join(CHANGE_LOG_DIR, summary_params["filename"]) if not os.path.exists(CHANGE_LOG_DIR): os.makedirs(CHANGE_LOG_DIR) - with open( - os.path.join(CHANGE_LOG_DIR, summary_params["filename"]), - "w", - encoding="UTF-8", - ) as file: - json.dump(changes, file, indent=4, default=str) + if len(changes) > 0 or not os.path.exists(json_filepath): + with open( + json_filepath, + "w", + encoding="UTF-8", + ) as file: + json.dump(changes, file, indent=4, default=str) summary_params["changes_summary"] = format_dict_to_markdown(summary) else: summary_params["filename"] = None @@ -318,6 +340,9 @@ def wrapper(*args, **kwargs): summary_params["verify_result"] = verify_result if not docs_only: + # Small delay to ensure CDC has captured all changes + # This is especially important for API-based corrections that use separate transactions + time.sleep(0.5) changes = get_changes_since_id(driver, id_before) if previous_cdc_setting != LOG_ENRICHMENT_DIFF: log.info( diff --git a/db-schema-migration/migrations/migration_002.py b/db-schema-migration/migrations/migration_002.py index e0cc1234..438a021f 100644 --- a/db-schema-migration/migrations/migration_002.py +++ b/db-schema-migration/migrations/migration_002.py @@ -1,4 +1,5 @@ """ Schema migrations needed for release to PROD post-February 2023.""" + import asyncio import os diff --git a/db-schema-migration/migrations/migration_008.py b/db-schema-migration/migrations/migration_008.py index 00475de5..13985b95 100644 --- a/db-schema-migration/migrations/migration_008.py +++ b/db-schema-migration/migrations/migration_008.py @@ -1,4 +1,5 @@ """ Schema migrations needed for release 1.8 to PROD post August 2024.""" + import os from migrations.common import migrate_ct_config_values, migrate_indexes_and_constraints diff --git a/db-schema-migration/migrations/migration_018.py b/db-schema-migration/migrations/migration_018.py index 8cede80c..0724be1b 100644 --- a/db-schema-migration/migrations/migration_018.py +++ b/db-schema-migration/migrations/migration_018.py @@ -1,8 +1,15 @@ """ Schema migrations needed for release 2.4 to PROD""" + import os from migrations.common import migrate_ct_config_values, migrate_indexes_and_constraints -from migrations.utils.utils import get_db_connection, get_db_driver, get_logger, run_cypher_query, print_counters_table +from migrations.utils.utils import ( + get_db_connection, + get_db_driver, + get_logger, + print_counters_table, + run_cypher_query, +) logger = get_logger(os.path.basename(__file__)) DB_DRIVER = get_db_driver() diff --git a/db-schema-migration/migrations/migration_019.py b/db-schema-migration/migrations/migration_019.py new file mode 100644 index 00000000..be24aa7f --- /dev/null +++ b/db-schema-migration/migrations/migration_019.py @@ -0,0 +1,120 @@ +"""Schema migrations needed for release 2.5 to PROD""" + +import os + +from migrations.common import migrate_ct_config_values, migrate_indexes_and_constraints +from migrations.utils.utils import ( + get_db_connection, + get_db_driver, + get_logger, + print_counters_table, + run_cypher_query, +) + +logger = get_logger(os.path.basename(__file__)) +DB_DRIVER = get_db_driver() +DB_CONNECTION = get_db_connection() +MIGRATION_DESC = "schema-migration-release-2.5" + + +def main(): + logger.info("Running migration on DB '%s'", os.environ["DATABASE_NAME"]) + ### Common migrations + migrate_indexes_and_constraints(DB_CONNECTION, logger) + migrate_ct_config_values(DB_CONNECTION, logger) + + ### Specific migrations + remove_odm_data(DB_DRIVER, logger) + migrate_codelist_ordinal(DB_DRIVER, logger) + + +def remove_odm_data(db_driver, log): + """ + Cleanup the database by removing ODM data. + This data is hard to migrate, and is not yet used. + We remove it from the database for now, to be re-imported at a later time. + """ + + contains_updates = False + for entity in [ + "OdmAlias", + "OdmDescription", + "OdmFormalExpression", + ]: + log.info(f"Cleaning up the database - Removing {entity} nodes") + _, summary = run_cypher_query( + db_driver, + f""" + MATCH (n:{entity}) + DETACH DELETE n + """, + ) + print_counters_table(summary.counters) + contains_updates = contains_updates or summary.counters.contains_updates + + for entity in [ + "OdmTemplate", + "OdmCondition", + "OdmMethod", + "OdmForm", + "OdmItemGroup", + "OdmItem", + "OdmStudyEvent", + "OdmVendorNamespace", + "OdmVendorAttribute", + "OdmVendorElement", + ]: + log.info( + f"Cleaning up the database - Removing {entity}Root, {entity}Value and {entity}Counter nodes" + ) + _, summary = run_cypher_query( + db_driver, + f""" + OPTIONAL MATCH (r:{entity}Root) + OPTIONAL MATCH (v:{entity}Value) + OPTIONAL MATCH (c:{entity}Counter) + DETACH DELETE r, v, c + """, + ) + print_counters_table(summary.counters) + contains_updates = contains_updates or summary.counters.contains_updates + + log.info("Cleaning up the database - Removing DeletedOdm* nodes") + _, summary = run_cypher_query( + db_driver, + """ + MATCH (n) + WHERE any(label IN labels(n) WHERE label STARTS WITH 'DeletedOdm') + OPTIONAL MATCH (n)-[r]-(m) + WHERE any(label IN labels(m) WHERE label STARTS WITH 'DeletedOdm') + DETACH DELETE n, r, m; + """, + ) + print_counters_table(summary.counters) + contains_updates = contains_updates or summary.counters.contains_updates + + return contains_updates + + +def migrate_codelist_ordinal(db_driver, log) -> bool: + """ + Migrate the codelist ordinal property + """ + + log.info("Migrating codelist ordinal property") + + _, summary = run_cypher_query( + db_driver, + """ + MATCH (n:CTCodelistAttributesValue) WHERE n.ordinal IS NOT NULL + SET n.is_ordinal = n.ordinal + REMOVE n.ordinal + RETURN count(n) + """, + ) + print_counters_table(summary.counters) + return summary.counters.contains_updates + + +if __name__ == "__main__": + main() diff --git a/db-schema-migration/migrations/migration_overview_019.md b/db-schema-migration/migrations/migration_overview_019.md new file mode 100644 index 00000000..da240d5d --- /dev/null +++ b/db-schema-migration/migrations/migration_overview_019.md @@ -0,0 +1,60 @@ +# Release: 2.5 (x 2026) + +## Common migrations + +### 1. Indexes and Constraints +------------------------------------- +#### Change Description +- Re-create all db indexes and constraints according to [db schema definition](https://orgremoved.visualstudio.com/Clinical-MDR/_git/neo4j-mdr-db?path=/db_schema.py&version=GBmain&_a=contents). + + +### 2. CT Config Values (Study Fields Configuration) +------------------------------------- +#### Change Description +- Re-create all `CTConfigValue` nodes according to values defined in [this file](https://orgremoved.visualstudio.com/Clinical-MDR/_git/studybuilder-import?path=/datafiles/configuration/study_fields_configuration.csv). + +#### Nodes Affected +- CTConfigValue + + +## Release specific migrations + +### 1. Removal of ODM data +------------------------------------- +#### Change Description +- Removing all ODM nodes including deleted nodes + +#### Nodes Affected +- `OdmAlias` +- `OdmDescription` +- `OdmFormalExpression` +- `OdmConditionRoot` +- `OdmConditionValue` +- `OdmMethodRoot` +- `OdmMethodValue` +- `OdmFormRoot` +- `OdmFormValue` +- `OdmItemGroupRoot` +- `OdmItemGroupValue` +- `OdmItemRoot` +- `OdmItemValue` +- `OdmStudyEventRoot` +- `OdmStudyEventValue` +- `OdmVendorNamespaceRoot` +- `OdmVendorNamespaceValue` +- `OdmVendorAttributeRoot` +- `OdmVendorAttributeValue` +- `OdmVendorElementRoot` +- `OdmVendorElementValue` + +### 2. Migrate codelist ordinal property +#### Change Description +- Rename the `ordinal` property on `CTCodelistAttributesValue` to `is_ordinal` + +#### Nodes Affected +- `CTCodelistAttributesValue` + +#### Relationships affected +- None + + diff --git a/db-schema-migration/migrations/utils/utils.py b/db-schema-migration/migrations/utils/utils.py index c96cf44a..c7aebda1 100644 --- a/db-schema-migration/migrations/utils/utils.py +++ b/db-schema-migration/migrations/utils/utils.py @@ -256,7 +256,7 @@ def api_post(path: str, payload: dict, params: Optional[Any] = None): url = API_BASE_URL + path logger.info("POST %s %s", url, params) res = requests.post( - url, json=payload, params=params, timeout=30, headers=API_HEADERS + url, json=payload, params=params, timeout=60, headers=API_HEADERS ) assert res.status_code in { 201, diff --git a/db-schema-migration/tests/test_correction_017.py b/db-schema-migration/tests/test_correction_017.py new file mode 100644 index 00000000..77172ee3 --- /dev/null +++ b/db-schema-migration/tests/test_correction_017.py @@ -0,0 +1,191 @@ +"""Data corrections for PROD: Test StudyVisits having correct properties based on visit_number/unique_visit_number.""" + +import os + +import pytest + +from data_corrections import correction_017 +from data_corrections.utils.utils import get_db_driver, save_md_title +from migrations.utils.utils import execute_statements, get_logger +from tests.data.db_before_correction_017 import ( + TEST_DATA_FIX_DUPLICATED_UNIQUE_VISIT_NUMBERS, + TEST_DATA_FIX_NOT_COHERENT_LIBRARY_SELECTION, + TEST_DATA_FIX_STUDIES_DIFFERENT_VERSIONS, + TEST_DATA_REMOVE_DUPLICATED_NON_VISIT, + TEST_DATA_REMOVE_SOA_CELL_WITHOUT_RELEASED, + TEST_DATA_REMOVE_STUDY_ACTION_BROKEN_AFTER, +) +from tests.utils.utils import clear_db +from verifications import correction_verification_017 + +LOGGER = get_logger(os.path.basename(__file__)) +DB_DRIVER = get_db_driver() + +VERIFY_RUN_LABEL = "test_verification" +CORRECTION_ARGS = (DB_DRIVER, LOGGER, VERIFY_RUN_LABEL) + + +@pytest.fixture(scope="session", autouse=True) +def setup_logging(): + """Initialize logging once at the start of the test session""" + desc = f"Running verification for data corrections on DB '{os.environ['DATABASE_NAME']}'" + save_md_title(VERIFY_RUN_LABEL, correction_017.__doc__, desc) + yield + + +def _setup_test_data(test_data): + """Helper to set up test data for a test""" + clear_db() + execute_statements(test_data) + + +def test_remove_duplicated_non_visit_and_unscheduled_visits(): + """Test remove_duplicated_non_visit_and_unscheduled_visits correction""" + # Setup test data + _setup_test_data(TEST_DATA_REMOVE_DUPLICATED_NON_VISIT) + + # Verify initial state (should fail) + with pytest.raises(AssertionError): + correction_verification_017.test_remove_duplicated_non_visit_and_unscheduled_visits() + + # Run correction + correction_017.remove_duplicated_non_visit_and_unscheduled_visits(*CORRECTION_ARGS) + + # Verify correction worked + correction_verification_017.test_remove_duplicated_non_visit_and_unscheduled_visits() + + +@pytest.mark.order(after="test_remove_duplicated_non_visit_and_unscheduled_visits") +def test_repeat_remove_duplicated_non_visit_and_unscheduled_visits(): + """Test that correction is idempotent""" + assert not correction_017.remove_duplicated_non_visit_and_unscheduled_visits( + *CORRECTION_ARGS + ) + + +def test_remove_study_action_with_broken_after(): + """Test remove_study_action_with_broken_after correction""" + # Setup test data + _setup_test_data(TEST_DATA_REMOVE_STUDY_ACTION_BROKEN_AFTER) + + # Verify initial state (should fail) + with pytest.raises(AssertionError): + correction_verification_017.test_remove_study_action_with_broken_after() + + # Run correction + correction_017.remove_study_action_with_broken_after(*CORRECTION_ARGS) + + # Verify correction worked + correction_verification_017.test_remove_study_action_with_broken_after() + + +@pytest.mark.order(after="test_remove_study_action_with_broken_after") +def test_repeat_remove_study_action_with_broken_after(): + """Test that correction is idempotent""" + assert not correction_017.remove_study_action_with_broken_after(*CORRECTION_ARGS) + + +def test_fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name(): + """Test fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name correction""" + # Setup test data + _setup_test_data(TEST_DATA_FIX_DUPLICATED_UNIQUE_VISIT_NUMBERS) + + # Verify initial state (should fail) + with pytest.raises(AssertionError): + correction_verification_017.test_fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name() + + # Run dependencies first + correction_017.remove_duplicated_non_visit_and_unscheduled_visits(*CORRECTION_ARGS) + # Run correction + correction_017.fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name( + *CORRECTION_ARGS + ) + + # Verify correction worked + correction_verification_017.test_fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name() + + +@pytest.mark.order( + after="test_fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name" +) +def test_repeat_fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name(): + """Test that correction is idempotent""" + assert not correction_017.fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name( + *CORRECTION_ARGS + ) + + +def test_fix_not_coherent_in_time_library_selection(): + """Test fix_not_coherent_in_time_library_selection correction""" + # Setup test data + _setup_test_data(TEST_DATA_FIX_NOT_COHERENT_LIBRARY_SELECTION) + + # Verify initial state (should fail) + with pytest.raises(AssertionError): + correction_verification_017.test_fix_not_coherent_in_time_library_selection() + + # Run correction + correction_017.fix_not_coherent_in_time_library_selection(*CORRECTION_ARGS) + + # Verify correction worked + correction_verification_017.test_fix_not_coherent_in_time_library_selection() + + +@pytest.mark.order(after="test_fix_not_coherent_in_time_library_selection") +def test_repeat_fix_not_coherent_in_time_library_selection(): + """Test that correction is idempotent""" + assert not correction_017.fix_not_coherent_in_time_library_selection( + *CORRECTION_ARGS + ) + + +def test_fix_studies_different_versions_with_the_same_start_date(): + """Test fix_studies_different_versions_with_the_same_start_date correction""" + # Setup test data + _setup_test_data(TEST_DATA_FIX_STUDIES_DIFFERENT_VERSIONS) + + # Verify initial state (should fail) + with pytest.raises(AssertionError): + correction_verification_017.test_fix_studies_different_versions_with_the_same_start_date() + + # Run correction + correction_017.fix_studies_different_versions_with_the_same_start_date( + *CORRECTION_ARGS + ) + + # Verify correction worked + correction_verification_017.test_fix_studies_different_versions_with_the_same_start_date() + + +@pytest.mark.order(after="test_fix_studies_different_versions_with_the_same_start_date") +def test_repeat_fix_studies_different_versions_with_the_same_start_date(): + """Test that correction is idempotent""" + assert not correction_017.fix_studies_different_versions_with_the_same_start_date( + *CORRECTION_ARGS + ) + + +def test_remove_soa_cell_relationships_without_released_study(): + """Test remove_soa_cell_relationships_without_released_study correction""" + # Setup test data + _setup_test_data(TEST_DATA_REMOVE_SOA_CELL_WITHOUT_RELEASED) + + # Verify initial state (should fail) + with pytest.raises(AssertionError): + correction_verification_017.test_remove_soa_cell_relationships_without_released_study() + + # Run correction + correction_017.remove_soa_cell_relationships_without_released_study( + *CORRECTION_ARGS + ) + + # Verify correction worked + correction_verification_017.test_remove_soa_cell_relationships_without_released_study() + + +@pytest.mark.order(after="test_remove_soa_cell_relationships_without_released_study") +def test_repeat_remove_soa_cell_relationships_without_released_study(): + """Test that correction is idempotent""" + assert not correction_017.remove_soa_cell_relationships_without_released_study( + *CORRECTION_ARGS + ) diff --git a/db-schema-migration/tests/test_migration_018.py b/db-schema-migration/tests/test_migration_018.py index 4c8f0d26..adf2e94a 100644 --- a/db-schema-migration/tests/test_migration_018.py +++ b/db-schema-migration/tests/test_migration_018.py @@ -49,11 +49,8 @@ def test_ct_config_values(migration): common.test_ct_config_values(db, logger) - def test_migrate_activity_grouping(migration): - logger.info( - "Verify activity grouping migration results" - ) + logger.info("Verify activity grouping migration results") records, _ = run_cypher_query( DB_DRIVER, @@ -83,6 +80,4 @@ def test_migrate_activity_grouping(migration): @pytest.mark.order(after="test_migrate_activity_grouping") def test_repeat_migrate_activity_grouping(migration): - assert not migration_018.migrate_activity_grouping( - DB_DRIVER, logger - ) + assert not migration_018.migrate_activity_grouping(DB_DRIVER, logger) diff --git a/db-schema-migration/tests/test_migration_019.py b/db-schema-migration/tests/test_migration_019.py new file mode 100644 index 00000000..4d017b24 --- /dev/null +++ b/db-schema-migration/tests/test_migration_019.py @@ -0,0 +1,97 @@ +import os + +import pytest + +from migrations import migration_019 +from migrations.utils.utils import ( + execute_statements, + get_db_connection, + get_db_driver, + get_logger, + run_cypher_query, +) +from tests import common +from tests.utils.utils import clear_db + +try: + from tests.data.db_before_migration_019 import TEST_DATA +except ImportError: + TEST_DATA = "" + + +# pylint: disable=unused-argument +# pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments +# pylint: disable=protected-access +# pylint: disable=broad-except + +# pytest fixture functions have other fixture functions as arguments, +# which pylint interprets as unused arguments + +db = get_db_connection() +DB_DRIVER = get_db_driver() +logger = get_logger(os.path.basename(__file__)) + + +@pytest.fixture(scope="module") +def initial_data(): + """Insert test data""" + clear_db() + execute_statements(TEST_DATA) + + +@pytest.fixture(scope="module") +def migration(initial_data): + # Run migration + migration_019.main() + + +def test_indexes_and_constraints(migration): + common.test_indexes_and_constraints(db, logger) + + +def test_ct_config_values(migration): + common.test_ct_config_values(db, logger) + + +def test_remove_odm_data(migration): + logger.info("Verify odm data removal migration results") + + records, _ = run_cypher_query( + DB_DRIVER, + """ + MATCH (n) + WHERE any(label IN labels(n) WHERE label STARTS WITH 'DeletedOdm' OR label STARTS WITH 'Odm') + RETURN n; + """, + ) + + assert ( + len(records) == 0 + ), "There must not exist any Odm* and DeletedOdm* nodes after migration" + + +@pytest.mark.order(after="test_remove_odm_data") +def test_repeat_remove_odm_data(migration): + assert not migration_019.remove_odm_data(DB_DRIVER, logger) + + +def test_migrate_codelist_ordinal(migration): + logger.info("Verify codelist ordinal migration results") + + records, _ = run_cypher_query( + DB_DRIVER, + """ + MATCH (n:CTCodelistAttributesValue) WHERE n.ordinal IS NOT NULL + RETURN count(n) AS count + """, + ) + + assert ( + records[0]["count"] == 0 + ), "There must not exist a CTCodelistAttributesValue with ordinal property after migration" + + +@pytest.mark.order(after="test_migrate_codelist_ordinal") +def test_repeat_migrate_codelist_ordinal(migration): + assert not migration_019.migrate_codelist_ordinal(DB_DRIVER, logger) diff --git a/db-schema-migration/verifications/correction_verification_016.py b/db-schema-migration/verifications/correction_verification_016.py index ba228c00..8de47940 100644 --- a/db-schema-migration/verifications/correction_verification_016.py +++ b/db-schema-migration/verifications/correction_verification_016.py @@ -184,6 +184,7 @@ def test_not_latest_has_version_lacks_end_date(): len(res) == 0 ), f"Found {len(res)} HAS_VERSION relationships that are not latest and lack an end date" + def test_remove_isolated_orphan_nodes(): """ Bug #3473052: Verify no isolated orphan nodes exist. diff --git a/db-schema-migration/verifications/correction_verification_017.py b/db-schema-migration/verifications/correction_verification_017.py new file mode 100644 index 00000000..69840ab3 --- /dev/null +++ b/db-schema-migration/verifications/correction_verification_017.py @@ -0,0 +1,208 @@ +""" +This modules verifies that database nodes/relations and API endpoints look and behave as expected. + +It utilizes tests written for verifying a specific migration, +without inserting any test data and without running any migration script on the target database. +""" + +import os + +from data_corrections.utils.utils import get_db_driver, run_cypher_query +from migrations.utils.utils import api_get, get_logger + +LOGGER = get_logger(os.path.basename(__file__)) +DB_DRIVER = get_db_driver() + + +def test_remove_duplicated_non_visit_and_unscheduled_visits(): + LOGGER.info( + "Checking for duplicated Non visits or Unscheduled visits", + ) + query = """ + MATCH (sr:StudyRoot)--(sv:StudyValue)-[:HAS_STUDY_VISIT]->(study_visit:StudyVisit) + WHERE NOT (study_visit)-[:BEFORE]-() AND study_visit.visit_class = "NON_VISIT" + WITH DISTINCT sr, collect(distinct study_visit.uid) as study_visit_uids + WHERE size(study_visit_uids) > 1 + RETURN sr.uid as study_uid, study_visit_uids + """ + res, _ = run_cypher_query(DB_DRIVER, query) + assert len(res) == 0, f"Found more than one Non visit, res:{res}" + + query = """ + MATCH (sr:StudyRoot)--(sv:StudyValue)-[:HAS_STUDY_VISIT]->(study_visit:StudyVisit) + WHERE NOT (study_visit)-[:BEFORE]-() AND study_visit.visit_class = "UNSCHEDULED_VISIT" + WITH DISTINCT sr, collect(distinct study_visit.uid) as study_visit_uids + WHERE size(study_visit_uids) > 1 + RETURN sr.uid as study_uid, study_visit_uids + """ + res, _ = run_cypher_query(DB_DRIVER, query) + assert len(res) == 0, f"Found more than one Unscheduled visit, res:{res}" + + +def test_fix_duplicated_unique_visit_numbers_and_incorrect_visit_number_visit_name_and_short_name(): + LOGGER.info( + "Verifying that StudyVisits unique visit numbers are unique", + ) + query = """ + MATCH (sr:StudyRoot)--(sv:StudyValue)-[:HAS_STUDY_VISIT]->(study_visit:StudyVisit) + WHERE NOT (study_visit)-[:BEFORE]-() + WITH DISTINCT sr, study_visit.unique_visit_number as unique_visit_number, collect(distinct study_visit.uid) as study_visit_uids + WHERE size(study_visit_uids) > 1 + RETURN sr.uid as study_uid, study_visit_uids, unique_visit_number + """ + res, _ = run_cypher_query(DB_DRIVER, query) + assert len(res) == 0, f"Found duplicated unique visit number, res:{res}" + + +def test_protocol_soa_response_status_code(): + """Verify that Protocol SoA API endpoint returns HTTP 200 status code for all released versions of all Studies.""" + + # Find all released Study versions of all Studies + results, _ = run_cypher_query( + DB_DRIVER, + """ + MATCH (study_root:StudyRoot)-[study_version:HAS_VERSION {status: 'RELEASED'}]->(study_value:StudyValue) + RETURN study_root.uid, study_version.version, exists((study_value)-[:HAS_STUDY_ACTIVITY]->()) AS has_activities + """, + ) + + # For all Studies and their versions + failed = [] + for result in results: + study_uid, study_version, has_activities = result + + # Check API endpoint returns Protocol SoA + LOGGER.info( + "Checking Protocol SoA for Study '%s' Version '%s'", + study_uid, + study_version, + ) + response = api_get( + f"/studies/{study_uid}/flowchart", + params={"study_value_version": study_version, "layout": "protocol"}, + check_ok_status=False, + ) + + if response.status_code == 404 and not has_activities: + # No activities in this Study version, SoA not expected + LOGGER.info( + "Study '%s' Version '%s' has no activities, Protocol SoA returned 404, it's OK.", + study_uid, + study_version, + ) + elif response.status_code != 200: + failed.append( + f"Study '{study_uid}' Version '{study_version}' {response.request.method} {response.request.url} : {response.status_code} {response.reason} {response.text[:1024]}" + ) + failed_items = '\n'.join(failed) + assert len(failed) == 0, f"Some Protocol SoA API calls failed: {failed_items}" + + +def test_remove_study_action_with_broken_after(): + """ + Verify that there are no StudyAction nodes without AFTER relationships. + StudyAction nodes (except UpdateSoASnapshot) must have an AFTER relationship. + """ + LOGGER.info("Checking for StudyAction nodes without AFTER relationships") + query = """ + MATCH (sr:StudyRoot)-[:AUDIT_TRAIL]->(sa:StudyAction) + WHERE + NOT (sa)-[:AFTER]->() + AND NOT sa:UpdateSoASnapshot + AND NOT (sa)-[:BEFORE]->(:StudyValue) + RETURN count(sa) AS broken_count + """ + res, _ = run_cypher_query(DB_DRIVER, query) + broken_count = res[0][0] if res else 0 + assert ( + broken_count == 0 + ), f"Found {broken_count} StudyAction nodes without AFTER relationships" + + +def test_fix_not_coherent_in_time_library_selection(): + """ + Verify that all StudyActivity nodes reference ActivityValue nodes that have + valid HAS_VERSION relationships at the time when the Create action was performed. + """ + LOGGER.info( + "Checking for StudyActivity nodes with non-coherent time library selections" + ) + query = """ + MATCH + (sr:StudyRoot )-[rel2:AUDIT_TRAIL]-> + (sa:Create)-[rel3:AFTER]-> + (sact:StudyActivity)-[rel4:HAS_SELECTED_ACTIVITY]- + (av:ActivityValue) + OPTIONAL MATCH + (av)<-[ahv:HAS_VERSION]- + (ar:ActivityRoot) + WHERE + ahv.start_date < sa.date + AND + (ahv.end_date IS NULL + OR ahv.end_date > sa.date) + AND (ahv.status = "Final" ) + WITH * WHERE ar IS NULL + RETURN count(distinct sa) AS broken_count + """ + res, _ = run_cypher_query(DB_DRIVER, query) + broken_count = res[0][0] if res else 0 + assert ( + broken_count == 0 + ), f"Found {broken_count} StudyActivity nodes with non-coherent time library selections" + + +def test_fix_studies_different_versions_with_the_same_start_date(): + """ + Verify that no StudyRoot has different versions with the same start_date. + The latest version should have a start_date that is greater than all previous versions. + """ + LOGGER.info( + "Checking for studies with different versions having the same start_date" + ) + query = """ + MATCH (root:StudyRoot)-[:LATEST]->(latest) + MATCH (root)-[v_latest:HAS_VERSION|LATEST_DRAFT|LATEST_FINAL|LATEST_LOCKED|LATEST_RETIRED|LATEST_RELEASED]->(latest) + WITH root, latest, v_latest.start_date as latest_start_date + MATCH (root)-[v_prev:HAS_VERSION|LATEST_DRAFT|LATEST_FINAL|LATEST_LOCKED|LATEST_RETIRED|LATEST_RELEASED]->(prev_value) + WHERE prev_value <> latest + AND v_prev.start_date >= latest_start_date + AND v_prev.start_date IS NOT NULL + RETURN count(DISTINCT root) AS broken_count + """ + res, _ = run_cypher_query(DB_DRIVER, query) + broken_count = res[0][0] if res else 0 + assert ( + broken_count == 0 + ), f"Found {broken_count} studies with different versions having the same or greater start_date compared to the latest version" + + +def test_remove_soa_cell_relationships_without_released_study(): + """ + Verify that no StudyValue nodes have HAS_PROTOCOL_SOA_CELL or HAS_PROTOCOL_SOA_FOOTNOTE + relationships when the StudyRoot doesn't have a RELEASED or LOCKED version. + """ + LOGGER.info( + "Checking for SOA cell/footnote relationships in studies without RELEASED or LOCKED versions" + ) + query = """ + MATCH + (sv:StudyValue)-[sv_ss]-(ss:StudySelection) + WHERE + TYPE(sv_ss)='HAS_PROTOCOL_SOA_CELL' + OR TYPE(sv_ss)='HAS_PROTOCOL_SOA_FOOTNOTE' + MATCH + (sv)-[versioning:HAS_VERSION]-(sr:StudyRoot) + WHERE NOT EXISTS( + (sr)-[:HAS_VERSION {status:'RELEASED'}]->() + ) + AND NOT EXISTS( + (sr)-[:HAS_VERSION {status:'LOCKED'}]->() + ) + RETURN count(DISTINCT sv_ss) AS broken_count + """ + res, _ = run_cypher_query(DB_DRIVER, query) + broken_count = res[0][0] if res else 0 + assert ( + broken_count == 0 + ), f"Found {broken_count} SOA cell/footnote relationships in studies without RELEASED or LOCKED versions" diff --git a/db-schema-migration/verifications/verification_018.py b/db-schema-migration/verifications/verification_018.py index a81eb07f..a913bde4 100644 --- a/db-schema-migration/verifications/verification_018.py +++ b/db-schema-migration/verifications/verification_018.py @@ -25,5 +25,6 @@ def test_ct_config_values(): def test_indexes_and_constraints(): test_migration_018.test_indexes_and_constraints(migration) + def test_migrate_activity_grouping(): test_migration_018.test_migrate_activity_grouping(migration) diff --git a/db-schema-migration/verifications/verification_019.py b/db-schema-migration/verifications/verification_019.py new file mode 100644 index 00000000..b85cfc1d --- /dev/null +++ b/db-schema-migration/verifications/verification_019.py @@ -0,0 +1,34 @@ +""" +This modules verifies that database nodes/relations and API endpoints look and behave as expected. + +It utilizes tests written for verifying a specific migration, +without inserting any test data and without running any migration script on the target database. +""" + +import pytest + +from tests import test_migration_019 + + +@pytest.fixture(scope="module") +def migration(): + """ + This method is empty as we do not want to run any migration script here. + We just wish to run all tests related to a specific migration. + """ + + +def test_ct_config_values(): + test_migration_019.test_ct_config_values(migration) + + +def test_indexes_and_constraints(): + test_migration_019.test_indexes_and_constraints(migration) + + +def test_remove_odm_data(): + test_migration_019.test_remove_odm_data(migration) + + +def test_migrate_codelist_ordinal(): + test_migration_019.test_migrate_codelist_ordinal(migration) diff --git a/documentation-portal/package.json b/documentation-portal/package.json index aaa0a479..86bbef4e 100644 --- a/documentation-portal/package.json +++ b/documentation-portal/package.json @@ -7,16 +7,14 @@ "license": "MIT", "private": true, "devDependencies": { - "vuepress": "^1.8.2" + "vuepress": "^1.8.2", + "@jamescoyle/vue-icon": "^0.1.2", + "@mdi/js": "^7.4.47" }, "scripts": { "docs:dev": "vuepress dev docs", "docs:build": "vuepress build docs", "update-config": "node scripts/update-config.js", - "build-sbom": "python3 assbom.py --yarn --append sbom-append.md --level 2 --output sbom.md" - }, - "dependencies": { - "@jamescoyle/vue-icon": "^0.1.2", - "@mdi/js": "^7.4.47" + "build-sbom": "python3 assbom.py --yarn --prepend sbom-prepend.md --level 2 --output sbom.md" } } diff --git a/documentation-portal/sbom-append.md b/documentation-portal/sbom-prepend.md similarity index 100% rename from documentation-portal/sbom-append.md rename to documentation-portal/sbom-prepend.md diff --git a/documentation-portal/sbom.md b/documentation-portal/sbom.md index 6dbdd934..735b8643 100644 --- a/documentation-portal/sbom.md +++ b/documentation-portal/sbom.md @@ -1,3 +1,9 @@ -No third-party licenses to include for this component. Created with [Vuepress](https://github.com/vuejs/vuepress) + +Created with [VuePress](https://github.com/vuejs/vuepress). + +## Installed packages + +No third-party packages were installed in the runtime environment. + diff --git a/mdr-standards-import/cdisc_staging.py b/mdr-standards-import/cdisc_staging.py index d602e8e2..7a9a7cd2 100644 --- a/mdr-standards-import/cdisc_staging.py +++ b/mdr-standards-import/cdisc_staging.py @@ -1942,6 +1942,44 @@ def make_sponsor_term_name(term_data): newname = "Follow-up" method = "Special case for 'Clinical Study Follow-up'" + # Updates related to codelist C66737 - Trial Phase Response. ICH M11 preferred term do not match NCI/CDISC preferred term + elif term_data["term_cid"] == "C54721": + # Rename Early Phase I -> Early Phase 1 + newname = "Early Phase 1" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C15600": + # Rename Phase I Trial -> Phase 1 + newname = "Phase 1" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C15693": + # Rename Phase I/II Trial -> Phase 1/Phase 2 + newname = "Phase 1/Phase 2" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C198366": + # Rename Phase I/II/III Trial -> Phase 1/Phase 2/Phase 3 + newname = "Phase 1/Phase 2/Phase 3" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C198367": + # Rename Phase I/III Trial -> Phase 1/Phase 3 + newname = "Phase 1/Phase 3" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C15601": + # Rename Phase II Trial -> Phase 2 + newname = "Phase 2" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C15694": + # Rename Phase II/III Trial -> Phase 2/Phase 3 + newname = "Phase 2/Phase 3" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C15602": + # Rename Phase III Trial -> Phase 3 + newname = "Phase 3" + method = "Special case for 'Trial Phase Response'" + elif term_data["term_cid"] == "C15603": + # Rename Phase IV Trial -> Phase 4 + newname = "Phase 4" + method = "Special case for 'Trial Phase Response'" + elif "C99077" in term_data["cl_cid"]: # Study type codelist diff --git a/neo4j-mdr-db/db_schema.py b/neo4j-mdr-db/db_schema.py index e81fc631..1eb46131 100644 --- a/neo4j-mdr-db/db_schema.py +++ b/neo4j-mdr-db/db_schema.py @@ -64,11 +64,10 @@ ("OdmVendorNamespaceValue", "name"), ("OdmVendorAttributeValue", "name"), ("OdmTemplateValue", "name"), - ("OdmDescriptionValue", "name"), ("OdmFormValue", "name"), ("OdmItemGroupValue", "name"), ("OdmItemValue", "name"), - ("OdmAliasValue", "name"), + ("OdmAlias", "name"), ("ObjectiveTemplateValue", "name"), ("ObjectiveValue", "name"), ("EndpointTemplateValue", "name"), @@ -117,8 +116,6 @@ ("OdmVendorElementValue", "name"), ("StudySourceVariable", "uid"), ("DataSupplierRoot", "uid"), - ("OdmAliasRoot", "uid"), - ("OdmDescriptionRoot", "uid"), ("DataSupplierValue", "name"), ] @@ -135,6 +132,20 @@ ("Notification", "title"), ] +# array of fulltext indexes to create [labels, properties, index_name] +FULLTEXT_INDEXES = [ + ( + ["CTCodelistAttributesValue", "CTCodelistNameValue"], + ["name", "submission_value"], + "codelist_fulltext_index", + ), + ( + ["CTTermAttributesValue", "CTTermNameValue"], + ["name", "submission_value"], + "term_fulltext_index", + ), +] + # array of relation indexes to create [type, property] REL_INDEXES = [ ("CONTAINS_DATASET", "href"), @@ -254,6 +265,16 @@ def build_create_node_text_index_query(data): return query +def build_create_node_fulltext_index_query(data): + labels, props, index_name = data + query = f""" + CREATE FULLTEXT INDEX {index_name} IF NOT EXISTS + FOR (n:{'|'.join(labels)}) + ON EACH [{', '.join([f'n.{prop}' for prop in props])}] + """ + return query + + def build_create_rel_index_query(data): label, prop = data name = label + "_" + prop @@ -294,6 +315,10 @@ def build_schema_queries(): query = build_create_node_text_index_query(idx) queries.append(query) + for idx in FULLTEXT_INDEXES: + query = build_create_node_fulltext_index_query(idx) + queries.append(query) + for idx in REL_INDEXES: query = build_create_rel_index_query(idx) queries.append(query) diff --git a/neo4j-mdr-db/init_aura.py b/neo4j-mdr-db/init_aura.py index c89f50e9..08e63540 100644 --- a/neo4j-mdr-db/init_aura.py +++ b/neo4j-mdr-db/init_aura.py @@ -187,7 +187,6 @@ def pre_load_counter_nodes(tx: Transaction): "OdmItemCounter", "OdmItemGroupCounter", "OdmStudyEventCounter", - "OdmTemplateCounter", "OdmVendorAttributeCounter", "OdmVendorNamespaceCounter", "PharmaceuticalProductCounter", @@ -253,7 +252,7 @@ def pre_load_counter_nodes(tx: Transaction): for query in build_schema_queries(): print(query) - session.run(querystring) + session.run(query) print( "\n-- Preloading TemplateParameter tree (Activity, Activity Group, Findings, Dose unit...) --" diff --git a/neo4j-mdr-db/init_neo4j.py b/neo4j-mdr-db/init_neo4j.py index 044bf707..6542e55c 100644 --- a/neo4j-mdr-db/init_neo4j.py +++ b/neo4j-mdr-db/init_neo4j.py @@ -194,7 +194,6 @@ def pre_load_counter_nodes(tx: Transaction): "OdmItemCounter", "OdmItemGroupCounter", "OdmStudyEventCounter", - "OdmTemplateCounter", "OdmVendorAttributeCounter", "OdmVendorNamespaceCounter", "PharmaceuticalProductCounter", diff --git a/neo4j-mdr-db/model/physical_data_model/neo4j-model.graphml b/neo4j-mdr-db/model/physical_data_model/neo4j-model.graphml index 64e32555..5bf802ce 100644 --- a/neo4j-mdr-db/model/physical_data_model/neo4j-model.graphml +++ b/neo4j-mdr-db/model/physical_data_model/neo4j-model.graphml @@ -1,6 +1,6 @@ - + @@ -2713,11 +2713,10 @@ multi_choice: Boolean - OdmDescription - language: String -description: String -instruction: Sting -sponsor_instruction: String + OdmTranslatedText + text_type: String +language: String +text: String @@ -7073,7 +7072,7 @@ COMPOUND_PARAMETER 1 - HAS_UNIT_DEFINITION + HAS_UNIT_DEFINITION 0..* @@ -9151,7 +9150,7 @@ is_default_linked: Boolean - IMPLEMENTS_VARIABLE + IMPLEMENTS_VARIABLE 0..* 0..* @@ -10544,7 +10543,7 @@ is_default_linked: Boolean - RELATED_CODELIST + HAS_VALID_CODELIST_FOR_ITEMS 0..* 0..* @@ -11465,7 +11464,7 @@ value: String - HAS_DESCRIPTION + HAS_TRANSLATED_TEXT @@ -11507,7 +11506,7 @@ value: String - HAS_DESCRIPTION + HAS_TRANSLATED_TEXT @@ -11532,7 +11531,7 @@ value: String - HAS_DESCRIPTION + HAS_TRANSLATED_TEXT @@ -11547,7 +11546,7 @@ value: String - HAS_DESCRIPTION + HAS_TRANSLATED_TEXT @@ -11589,7 +11588,7 @@ value: String - HAS_DESCRIPTION + HAS_TRANSLATED_TEXT @@ -11613,7 +11612,7 @@ value: String - HAS_DESCRIPTION + HAS_TRANSLATED_TEXT @@ -11651,8 +11650,8 @@ value: String - - <svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="30" height="30"> + + <svg xmlns="http://www.w3.org/2000/svg" version="1.0" width="30" height="30"> <g> <path d="M30,15A15,15,0,1,1,0,15A15,15,0,1,1,30,15Z" fill="#0055d4"/> <path d="M25.9545,15A10.9545,10.9545,0,1,1,4.0455,15A10.9545,10.9545,0,1,1,25.9545,15Z" fill="#ffffff"/> diff --git a/neo4j-mdr-db/neodash/neo4j_custom_procedures/cypher_activity_library_content_dashboard.cypher b/neo4j-mdr-db/neodash/neo4j_custom_procedures/cypher_activity_library_content_dashboard.cypher new file mode 100644 index 00000000..e6fe571e --- /dev/null +++ b/neo4j-mdr-db/neodash/neo4j_custom_procedures/cypher_activity_library_content_dashboard.cypher @@ -0,0 +1,169 @@ +/******************************************************************************* + * UPDATED QUERIES - Activity Library Content Dashboard + * + * Report: Activity Library Dashboard + * Version: 2.4 + * Source: neodash/neodash_reports/activity_library_content_dashboard.json + * + * This file contains UPDATED versions of queries affected by schema changes. + * Only queries using the old ActivityValidGroup node have been updated. + * All queries are ready to copy-paste into Neo4j Browser. + * + * Date: January 20, 2026 + ******************************************************************************/ + + +/*============================================================================== + * PAGE: Activity Lib (Search Top-Down) + * TITLE: Number of Activities (Instances per Activity, when Subgroup is Selected) + * + * FULL QUERY with apoc.case - Updated DEFAULT FALLBACK branch + *============================================================================*/ + +// only instance class selected +CALL apoc.case([not $neodash_activityinstanceclassvalue_name='' and $neodash_activityinstanceclassvalue_name_subtype='' and $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) +MATCH (g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-() +MATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) +MATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a] +return aic.name as Category, +count(distinct act) as `Number of Activities` order by Category' , + +// instance class and subclass selected +not $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) +MATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-() +MATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b] +MATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a] +return agrp.name as Category, +count(distinct act) as `Number of Activities` order by Category', + +// instance class, subclass and group selected +not $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and not $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) where agrp.name in [$c] +MATCH (g)-[:HAS_SELECTED_SUBGROUP]-(asgrp:ActivitySubGroupValue) +MATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-() +MATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b] +MATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a] +return asgrp.name as Category, +count(distinct act) as `Number of Activities` order by Category', + +// all fields selected +not $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and not $neodash_activitygroupvalue_name='' and not $neodash_activitysubgroupvalue_name='', +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue), +(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) where agrp.name in [$c] and asgrp.name in[$d] +MATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-() +MATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b] +MATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a] +return act.name as Category, +count(distinct ai) as `Number of Activities` order by Category'], + +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue), +(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) +MATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-() +MATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) +MATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) +return aicp.name as Category, +count(distinct act) as `Number of Activities` order by Category', +{a:$neodash_activityinstanceclassvalue_name, +b:$neodash_activityinstanceclassvalue_name_subtype, +c:$neodash_activitygroupvalue_name, +d:$neodash_activitysubgroupvalue_name}) YIELD value +return value.Category as Category, +value.`Number of Activities` as`Number of Activities/Instances` order by Category; + + +/*============================================================================== + * PAGE: Activity Lib (Search Top-Down) + * TITLE: List of Activities + * + * FULL QUERY with apoc.case - Updated branches 2, 3, and DEFAULT FALLBACK + *============================================================================*/ + +CALL apoc.case([not $neodash_activityinstanceclassvalue_name='' + and $neodash_activityinstanceclassvalue_name_subtype='' + and $neodash_activitygroupvalue_name='' + and $neodash_activitysubgroupvalue_name='', + +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue), +(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue), +(g)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(), +(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue), +(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue), +(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue), +(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue) +where aicp.name in [$a] + +return distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', + +not $neodash_activityinstanceclassvalue_name='' +and not $neodash_activityinstanceclassvalue_name_subtype='' +and $neodash_activitygroupvalue_name='' +and $neodash_activitysubgroupvalue_name='', + +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue), +(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue), +(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(), +(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue), +(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue), +(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue), +(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue) +where aicp.name in [$a] and aic.name in [$b] + +return distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', + +not $neodash_activityinstanceclassvalue_name='' +and not $neodash_activityinstanceclassvalue_name_subtype='' +and not $neodash_activitygroupvalue_name='' +and $neodash_activitysubgroupvalue_name='', + +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue), +(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue), +(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(), +(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue), +(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue), +(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue), +(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue) +where aicp.name in [$a] and aic.name in [$b] and agrp.name in [$c] + +return distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', + +not $neodash_activityinstanceclassvalue_name='' +and not $neodash_activityinstanceclassvalue_name_subtype='' +and not $neodash_activitygroupvalue_name='' +and not $neodash_activitysubgroupvalue_name='', + +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue), +(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue), +(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(), +(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue), +(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue), +(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue), +(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue) +where aicp.name in [$a] and aic.name in [$b] and agrp.name in [$c] and asgrp.name in [$d] + +return distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity'], + +'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue), +(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue), +(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(), +(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue), +(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue), +(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue), +(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue) +return distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity',{ +a:$neodash_activityinstanceclassvalue_name, +b:$neodash_activityinstanceclassvalue_name_subtype, +c:$neodash_activitygroupvalue_name, +d:$neodash_activitysubgroupvalue_name}) YIELD value return value.ActivityType as `Activity Type`,value.ActivitySubType as `Activity Subtype`, value.ActivityGroup as `Activity Group` ,value.ActivitySubGroup as `Activity Subgroup` , value.Activity as Activity order by Activity; + + +/******************************************************************************* + * END OF FILE + * + * SUMMARY: + * -------- + * - 2 queries updated for new schema (removing ActivityValidGroup node) + * - All other queries in the dashboard already use the new schema + * - Ready to copy-paste into Neo4j Browser for testing + ******************************************************************************/ diff --git a/neo4j-mdr-db/neodash/neodash_reports/activity_instance_export.json b/neo4j-mdr-db/neodash/neodash_reports/activity_instance_export.json new file mode 100644 index 00000000..c3ad7d74 --- /dev/null +++ b/neo4j-mdr-db/neodash/neodash_reports/activity_instance_export.json @@ -0,0 +1,96 @@ +{ + "title": "Activity Instance Export", + "version": "2.4", + "settings": { + "pagenumber": 0, + "editable": true, + "fullscreenEnabled": false, + "parameters": { + "neodash_activityinstancevalue_name": [ + "Eosinophils Blood" + ], + "neodash_activityinstancevalue_name_display": [ + "Eosinophils Blood" + ], + "neodash_": "finding_category" + }, + "theme": "light" + }, + "pages": [ + { + "title": "New page", + "reports": [ + { + "id": "fd7ccd21-1a54-4645-ae33-a3c455e0b449", + "title": "Activity items", + "query": "MATCH (n1:ActivityInstanceValue)\nWHERE n1.name in $neodash_activityinstancevalue_name\nMATCH (n1)-[r1:CONTAINS_ACTIVITY_ITEM]-(ai:ActivityItem)\n\nMATCH\n (ai)-[r2:HAS_ACTIVITY_ITEM]-\n (n6:ActivityItemClassRoot)-[r3:LATEST_FINAL]-\n (n7:ActivityItemClassValue)\nMATCH (n1)-[r4:LATEST_FINAL]-(n8:ActivityInstanceRoot)\nMATCH\n (n1)-[r5:HAS_ACTIVITY]-\n (n9:ActivityGrouping)-[r6:HAS_GROUPING]-\n (n10:ActivityValue)-[r7:LATEST_FINAL]-\n (n11:ActivityRoot)\nMATCH\n (n1)-[r8:ACTIVITY_INSTANCE_CLASS]-\n (n12:ActivityInstanceClassRoot)-[r9:LATEST_FINAL]-\n (n13:ActivityInstanceClassValue)\n\n\n\n// Make sure that the query works in both previous (PRD) and new (DEV) model\n// Previous model: ActivityGrouping-[r10:IN_SUBGROUP]-(ActivityValidGroup)-[r11:IN_GROUP]-(ActivityGroupValue)\nOPTIONAL MATCH\n (n9)-[r12:IN_SUBGROUP]-\n (old_avg:ActivityValidGroup)-[r13:IN_GROUP]-\n (old_agv:ActivityGroupValue)\nOPTIONAL MATCH (old_avg)-[r14:HAS_GROUP]-(old_asgv:ActivitySubGroupValue)\n// New model: ActivityGrouping-[r15:HAS_SELECTED_GROUP]-(ActivityGroupValue)\nOPTIONAL MATCH\n (n9)-[r16:HAS_SELECTED_GROUP]-\n (new_agv:ActivityGroupValue)\nOPTIONAL MATCH (n9)-[r17:HAS_SELECTED_SUBGROUP]-(new_asgv:ActivitySubGroupValue)\n\nOPTIONAL MATCH\n (ai)-[r18:HAS_CT_TERM]->\n (n3:CTTermRoot)-[r19:HAS_ATTRIBUTES_ROOT]->\n (n4:CTTermAttributesRoot)-[r20:LATEST_FINAL]-\n (n5:CTTermAttributesValue)\nOPTIONAL MATCH\n (ai)-[r21:HAS_UNIT_DEFINITION]->\n (udr:UnitDefinitionRoot)-[r22:LATEST_FINAL]->\n (udv:UnitDefinitionValue)\n\nWITH\n COALESCE(collect(new_agv.name)[0], collect(old_agv.name)[0]) AS `Assm. group`,\n COALESCE(collect(new_asgv.name)[0], collect(old_asgv.name)[0]) AS `Assm. subgroup`,\n n10.name AS activity,\n n1.name AS activity_instance,\n n13.name AS ActivityInstanceClassValue,\n n1.topic_code AS TOPIC_CD,\n n7.name AS activity_item_type,\n COALESCE(n5.name_submission_value, n5.code_submission_value, udv.name) AS term\nRETURN\n `Assm. group`,\n `Assm. subgroup`,\n activity,\n activity_instance,\n ActivityInstanceClassValue,\n TOPIC_CD,\n activity_item_type,\n \"\" as codelist,\n apoc.text.join(collect(DISTINCT term), '|') AS activity_item_value\nORDER BY TOPIC_CD\n", + "width": 21, + "height": 6, + "x": 0, + "y": 2, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "wrapContent": true, + "allowDownload": true, + "actionsRules": [ + { + "condition": "Click", + "field": "", + "value": "", + "customization": "set variable", + "customizationValue": "" + } + ], + "styleRules": [ + { + "field": "", + "condition": "=", + "value": "", + "customization": "row color", + "customizationValue": "black" + } + ] + } + }, + { + "id": "972e858e-4a51-4465-ab27-8fca527465f2", + "title": "Select Activity Instances", + "query": "MATCH (n:`ActivityInstanceValue`) \nWHERE toLower(toString(n.`name`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`name` as value, n.`name` as display ORDER BY size(toString(value)) ASC LIMIT 5", + "width": 21, + "height": 2, + "x": 0, + "y": 0, + "type": "select", + "selection": {}, + "settings": { + "type": "Node Property", + "entityType": "ActivityInstanceValue", + "propertyType": "name", + "propertyTypeDisplay": "name", + "parameterName": "neodash_activityinstancevalue_name", + "multiSelector": true, + "multiSelectLimit": 20 + }, + "schema": [] + } + ] + } + ], + "parameters": {}, + "extensions": { + "active": true, + "activeReducers": [], + "advanced-charts": { + "active": true + }, + "styling": { + "active": true + }, + "actions": { + "active": true + } + }, + "uuid": "be1726c5-b9a1-4b07-ab16-5eb7c6a69345" +} \ No newline at end of file diff --git a/neo4j-mdr-db/neodash/neodash_reports/activity_library_content_dashboard.json b/neo4j-mdr-db/neodash/neodash_reports/activity_library_content_dashboard.json index 5faadb59..36a8573d 100644 --- a/neo4j-mdr-db/neodash/neodash_reports/activity_library_content_dashboard.json +++ b/neo4j-mdr-db/neodash/neodash_reports/activity_library_content_dashboard.json @@ -191,7 +191,7 @@ }, { "title": "Number of Activities (Instances per Activity, when Subgroup is Selected)", - "query": "// only instance class selected\nCALL apoc.case([not $neodash_activityinstanceclassvalue_name='' and $neodash_activityinstanceclassvalue_name_subtype='' and $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue)\nMATCH (g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue)\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn aic.name as Category,\ncount(distinct act) as `Number of Activities` order by Category' ,\n\n// instance class and subclass selected\nnot $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue)\nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b]\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn agrp.name as Category,\ncount(distinct act) as `Number of Activities` order by Category',\n\n// instance class, subclass and group selected\nnot $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and not $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) where agrp.name in [$c]\nMATCH (g)-[:HAS_SELECTED_SUBGROUP]-(asgrp:ActivitySubGroupValue)\nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b]\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn asgrp.name as Category,\ncount(distinct act) as `Number of Activities` order by Category',\n\n// all fields selected\nnot $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and not $neodash_activitygroupvalue_name='' and not $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) where agrp.name in [$c] and asgrp.name in[$d]\nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b]\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn act.name as Category,\ncount(distinct ai) as `Number of Activities` order by Category'],\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:IN_SUBGROUP]->(sg:ActivityValidGroup)<-[R3:HAS_GROUP]-(asgrp:ActivitySubGroupValue),\n(sg)-[R4:IN_GROUP]->(agrp:ActivityGroupValue) \nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) \nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) \nreturn aicp.name as Category,\ncount(distinct act) as `Number of Activities` order by Category',\n{a:$neodash_activityinstanceclassvalue_name,\nb:$neodash_activityinstanceclassvalue_name_subtype,\nc:$neodash_activitygroupvalue_name, \nd:$neodash_activitysubgroupvalue_name}) YIELD value \nreturn value.Category as Category,\nvalue.`Number of Activities` as`Number of Activities/Instances` order by Category\n\n", + "query": "// only instance class selected\nCALL apoc.case([not $neodash_activityinstanceclassvalue_name='' and $neodash_activityinstanceclassvalue_name_subtype='' and $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue)\nMATCH (g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue)\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn aic.name as Category,\ncount(distinct act) as `Number of Activities` order by Category' ,\n\n// instance class and subclass selected\nnot $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue)\nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b]\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn agrp.name as Category,\ncount(distinct act) as `Number of Activities` order by Category',\n\n// instance class, subclass and group selected\nnot $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and not $neodash_activitygroupvalue_name='' and $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R3:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) where agrp.name in [$c]\nMATCH (g)-[:HAS_SELECTED_SUBGROUP]-(asgrp:ActivitySubGroupValue)\nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b]\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn asgrp.name as Category,\ncount(distinct act) as `Number of Activities` order by Category',\n\n// all fields selected\nnot $neodash_activityinstanceclassvalue_name='' and not $neodash_activityinstanceclassvalue_name_subtype='' and not $neodash_activitygroupvalue_name='' and not $neodash_activitysubgroupvalue_name='', \n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) where agrp.name in [$c] and asgrp.name in[$d]\nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) where aic.name in [$b]\nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) where aicp.name in [$a]\nreturn act.name as Category,\ncount(distinct ai) as `Number of Activities` order by Category'],\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue) \nMATCH(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-()\nMATCH (ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue) \nMATCH (aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue) \nreturn aicp.name as Category,\ncount(distinct act) as `Number of Activities` order by Category',\n{a:$neodash_activityinstanceclassvalue_name,\nb:$neodash_activityinstanceclassvalue_name_subtype,\nc:$neodash_activitygroupvalue_name, \nd:$neodash_activitysubgroupvalue_name}) YIELD value \nreturn value.Category as Category,\nvalue.`Number of Activities` as`Number of Activities/Instances` order by Category\n\n", "width": 24, "height": 6, "x": 0, @@ -790,7 +790,7 @@ { "id": "c1bb190a-b3a9-4e65-843d-c0ab8c42ba42", "title": "List of Activities", - "query": "CALL apoc.case([not $neodash_activityinstanceclassvalue_name='' \n and $neodash_activityinstanceclassvalue_name_subtype='' \n and $neodash_activitygroupvalue_name='' \n and $neodash_activitysubgroupvalue_name='', \n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', \n\nnot $neodash_activityinstanceclassvalue_name='' \nand not $neodash_activityinstanceclassvalue_name_subtype='' \nand $neodash_activitygroupvalue_name='' \nand $neodash_activitysubgroupvalue_name='', \n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:IN_SUBGROUP]->(sg:ActivityValidGroup)<-[R3:HAS_GROUP]-(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] and aic.name in [$b] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', \n\nnot $neodash_activityinstanceclassvalue_name='' \nand not $neodash_activityinstanceclassvalue_name_subtype='' \nand not $neodash_activitygroupvalue_name='' \nand $neodash_activitysubgroupvalue_name='', \n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:IN_SUBGROUP]->(sg:ActivityValidGroup)<-[R3:HAS_GROUP]-(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] and aic.name in [$b] and agrp.name in [$c] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', \n\nnot $neodash_activityinstanceclassvalue_name='' \nand not $neodash_activityinstanceclassvalue_name_subtype='' \nand not $neodash_activitygroupvalue_name='' \nand not $neodash_activitysubgroupvalue_name='',\n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] and aic.name in [$b] and agrp.name in [$c] and asgrp.name in [$d] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity'],\n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:IN_SUBGROUP]->(sg:ActivityValidGroup)<-[R3:HAS_GROUP]-(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue) \nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity',{\na:$neodash_activityinstanceclassvalue_name, \nb:$neodash_activityinstanceclassvalue_name_subtype,\nc:$neodash_activitygroupvalue_name, \nd:$neodash_activitysubgroupvalue_name}) YIELD value return value.ActivityType as `Activity Type`,value.ActivitySubType as `Activity Subtype`, value.ActivityGroup as `Activity Group` ,value.ActivitySubGroup as `Activity Subgroup` , value.Activity as Activity order by Activity", + "query": "CALL apoc.case([not $neodash_activityinstanceclassvalue_name='' \n and $neodash_activityinstanceclassvalue_name_subtype='' \n and $neodash_activitygroupvalue_name='' \n and $neodash_activitysubgroupvalue_name='', \n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', \n\nnot $neodash_activityinstanceclassvalue_name='' \nand not $neodash_activityinstanceclassvalue_name_subtype='' \nand $neodash_activitygroupvalue_name='' \nand $neodash_activitysubgroupvalue_name='', \n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] and aic.name in [$b] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', \n\nnot $neodash_activityinstanceclassvalue_name='' \nand not $neodash_activityinstanceclassvalue_name_subtype='' \nand not $neodash_activitygroupvalue_name='' \nand $neodash_activitysubgroupvalue_name='', \n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] and aic.name in [$b] and agrp.name in [$c] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity', \n\nnot $neodash_activityinstanceclassvalue_name='' \nand not $neodash_activityinstanceclassvalue_name_subtype='' \nand not $neodash_activitygroupvalue_name='' \nand not $neodash_activitysubgroupvalue_name='',\n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue)\nwhere aicp.name in [$a] and aic.name in [$b] and agrp.name in [$c] and asgrp.name in [$d] \n\nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity'],\n\n'MATCH(act:ActivityValue)-[R1:HAS_GROUPING]->(g:ActivityGrouping)-[R2:HAS_SELECTED_SUBGROUP]->(asgrp:ActivitySubGroupValue),\n(g)-[R4:HAS_SELECTED_GROUP]->(agrp:ActivityGroupValue),\n(g:ActivityGrouping)<-[R5:HAS_ACTIVITY]-(ai:ActivityInstanceValue)<-[:LATEST]-(),\n(ai)-[R42:ACTIVITY_INSTANCE_CLASS]->(aicr:ActivityInstanceClassRoot)-[R43:LATEST]->(aic:ActivityInstanceClassValue),\n(aitm1)<-[R8:HAS_ACTIVITY_ITEM]-(aitmc1r:ActivityItemClassRoot)-[R9:LATEST]->(aitmc1:ActivityItemClassValue),\n(aicr)-[R10:PARENT_CLASS]->(aicrp:ActivityInstanceClassRoot)-[R11:LATEST]->(aicp:ActivityInstanceClassValue),\n(aicrp)-[R12:PARENT_CLASS]->(aicrpp:ActivityInstanceClassRoot)-[R13:LATEST]->(aicpp:ActivityInstanceClassValue) \nreturn distinct aicp.name as ActivityType, aic.name as ActivitySubType, agrp.name as ActivityGroup, asgrp.name as ActivitySubGroup, act.name as Activity',{\na:$neodash_activityinstanceclassvalue_name, \nb:$neodash_activityinstanceclassvalue_name_subtype,\nc:$neodash_activitygroupvalue_name, \nd:$neodash_activitysubgroupvalue_name}) YIELD value return value.ActivityType as `Activity Type`,value.ActivitySubType as `Activity Subtype`, value.ActivityGroup as `Activity Group` ,value.ActivitySubGroup as `Activity Subgroup` , value.Activity as Activity order by Activity", "width": 24, "height": 4, "x": 0, diff --git a/neo4j-mdr-db/neodash/neodash_reports/crf-impact-analysis.json b/neo4j-mdr-db/neodash/neodash_reports/crf-impact-analysis.json new file mode 100644 index 00000000..b2797f09 --- /dev/null +++ b/neo4j-mdr-db/neodash/neodash_reports/crf-impact-analysis.json @@ -0,0 +1,457 @@ +{ + "title": "CRF Impact Analysis", + "version": "2.4", + "settings": { + "pagenumber": 0, + "editable": true, + "fullscreenEnabled": false, + "parameters": { + "neodash_odmformvalue_oid_impact": "", + "neodash_odmformvalue_oid_impact_display": "", + "neodash_form_version_impact": "", + "neodash_form_version_impact_display": "", + "neodash_odmitemgroupvalue_oid_impact": "", + "neodash_odmitemgroupvalue_oid_impact_display": "", + "neodash_group_version_impact": "", + "neodash_group_version_impact_display": "", + "neodash_odmitemvalue_oid_impact": "", + "neodash_odmitemvalue_oid_impact_display": "", + "neodash_item_version_impact": "", + "neodash_item_version_impact_display": "" + }, + "theme": "light" + }, + "pages": [ + { + "title": "ReadMe", + "reports": [ + { + "id": "00a7fa67-6127-495e-8d30-abe699ca2806", + "title": "Guide", + "query": "This report provides impact analysis for CRF library components.\n\n**Item Impact Analysis**\n- Select an Item and Version to see:\n - All ItemGroups that reference this Item\n - All Forms that contain these ItemGroups\n - Collections where these Forms are used\n - Full dependency chain showing impact\n \n**ItemGroup Impact Analysis**\n- Select an ItemGroup and Version to see:\n - All Items that are part of this ItemGroup\n - All Forms that reference this ItemGroup\n - Collections where these Forms are used\n - Potential downstream impact of changes\n\n**Form Impact Analysis**\n- Select a Form and Version to see:\n - All ItemGroups that are part of this Form\n - All Items within those ItemGroups\n - Collections where this Form is used\n - Potential downstream impact of changes\n\n\nThis report helps users understand the ripple effect of changes across the CRF library structure.", + "width": 24, + "height": 4, + "x": 0, + "y": 0, + "type": "text", + "selection": {}, + "settings": {} + } + ] + }, + { + "title": "Item Impact", + "reports": [ + { + "id": "3a3a3333-3333-3333-3333-333333333331", + "title": "Select Item", + "query": "MATCH (n:`OdmItemValue`) \nWHERE toLower(toString(n.`oid`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`oid` as value, n.`name` as display ORDER BY size(toString(value)) ASC LIMIT 1000", + "width": 8, + "height": 2, + "x": 0, + "y": 0, + "type": "select", + "selection": {}, + "settings": { + "type": "Custom Query", + "refreshButtonEnabled": true, + "helperText": "Select an Item to analyze", + "entityType": "odmitemvalue_oid_impact", + "parameterName": "neodash_odmitemvalue_oid_impact" + } + }, + { + "id": "3a3a3333-3333-3333-3333-333333333332", + "title": "Select Item Version", + "query": "MATCH (item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nWHERE item.oid = $neodash_odmitemvalue_oid_impact\nRETURN item_version.version as value, 'Version ' + item_version.version as display\nORDER BY item_version.version DESC", + "width": 8, + "height": 2, + "x": 0, + "y": 2, + "type": "select", + "selection": {}, + "settings": { + "type": "Custom Query", + "refreshButtonEnabled": true, + "helperText": "Select Item Version", + "entityType": "item_version_impact", + "parameterName": "neodash_item_version_impact" + } + }, + { + "id": "3a3a3333-3333-3333-3333-333333333333", + "title": "Item Details", + "query": "MATCH (item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nWHERE item.oid = $neodash_odmitemvalue_oid_impact \n AND item_version.version = $neodash_item_version_impact\nOPTIONAL MATCH (item)-[:HAS_DESCRIPTION]->(desc:OdmDescription)\nOPTIONAL MATCH (item)-[:HAS_CODELIST_TERM]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(codelist:CTCodelistTerm)\nWITH item, item_version, desc, collect(DISTINCT codelist.submission_value) AS codelists\nRETURN \n item.oid AS `Item OID`,\n item.name AS `Item Name`,\n item_version.version AS Version,\n item.data_type AS `Data Type`,\n item.length AS Length,\n item.significant_digits AS `Significant Digits`,\n CASE WHEN size(codelists) > 0 THEN apoc.text.join(codelists, '; ') ELSE null END AS `Codelist Terms`,\n desc.text AS Description", + "width": 7, + "height": 4, + "x": 8, + "y": 0, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "transposed": true, + "refreshButtonEnabled": true, + "wrapContent": true + } + }, + { + "id": "3a3a3333-3333-3333-3333-333333333334", + "title": "ItemGroups using Item - $neodash_odmitemvalue_oid_impact (v $neodash_item_version_impact)", + "query": "MATCH (item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nWHERE item.oid = $neodash_odmitemvalue_oid_impact \n AND item_version.version = $neodash_item_version_impact\nOPTIONAL MATCH (group_root:OdmItemGroupRoot)-[group_version:HAS_VERSION]->(group:OdmItemGroupValue)-[item_ref:ITEM_REF]->(item)\nOPTIONAL MATCH (group)-[:HAS_SDTM_DOMAIN]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(domain:CTCodelistTerm)\nRETURN \n group.oid AS `ItemGroup OID`,\n group.name AS `ItemGroup Name`,\n group_version.version AS `ItemGroup Version`,\n domain.submission_value AS `SDTM Domain`,\n item_ref.mandatory AS `Mandatory in Group`,\n item_ref.order_number AS `Order in Group`\nORDER BY `ItemGroup Name`, `ItemGroup Version` DESC", + "width": 13, + "height": 6, + "x": 10, + "y": 7, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "noDataMessage": "Select an Item and Version above", + "wrapContent": true + } + }, + { + "id": "3a3a3333-3333-3333-3333-333333333335", + "title": "Forms using Item - $neodash_odmitemvalue_oid_impact (v $neodash_item_version_impact)", + "query": "MATCH (item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nWHERE item.oid = $neodash_odmitemvalue_oid_impact \n AND item_version.version = $neodash_item_version_impact\nOPTIONAL MATCH (form_root:OdmFormRoot)-[form_version:HAS_VERSION]->(form:OdmFormValue)-[:ITEM_GROUP_REF]->(group:OdmItemGroupValue)-[:ITEM_REF]->(item)\nRETURN DISTINCT\n form.oid AS `Form OID`,\n form.name AS `Form Name`,\n form_version.version AS `Form Version`,\n group.name AS `Via ItemGroup`\nORDER BY `Form Name`, `Form Version` DESC", + "width": 10, + "height": 6, + "x": 0, + "y": 7, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "noDataMessage": "Select an Item and Version above", + "wrapContent": true + } + }, + { + "id": "3a3a3333-3333-3333-3333-333333333336", + "title": "Collections using Item - $neodash_odmitemvalue_oid_impact", + "query": "MATCH (item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nWHERE item.oid = $neodash_odmitemvalue_oid_impact \n AND item_version.version = $neodash_item_version_impact\nOPTIONAL MATCH (coll_root:OdmStudyEventRoot)-[coll_version:HAS_VERSION]->(coll:OdmStudyEventValue)-[:FORM_REF]->(form:OdmFormValue)-[:ITEM_GROUP_REF]->(group:OdmItemGroupValue)-[:ITEM_REF]->(item)\nRETURN DISTINCT\n coll.name AS `Collection Name`,\n coll_version.version AS `Collection Version`,\n form.name AS `Via Form`,\n group.name AS `Via ItemGroup`\nORDER BY `Collection Name`, `Collection Version` DESC, `Via Form`", + "width": 23, + "height": 3, + "x": 0, + "y": 13, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "fullscreenEnabled": true, + "allowDownload": true, + "noDataMessage": "Select an Item and Version above", + "wrapContent": true + } + }, + { + "id": "3a3a3333-3333-3333-3333-333333333337", + "title": "Dependency Chain - $neodash_odmitemvalue_oid_impact (v $neodash_item_version_impact)", + "query": "MATCH (item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nWHERE item.oid = $neodash_odmitemvalue_oid_impact \n AND item_version.version = $neodash_item_version_impact\n\nOPTIONAL MATCH (coll:OdmStudyEventValue)-[:FORM_REF]->(form:OdmFormValue)-[:ITEM_GROUP_REF]->(group:OdmItemGroupValue)-[:ITEM_REF]->(item)\n\nWITH item, \n coll.name AS coll_name,\n form.name AS form_name,\n group.name AS group_name\n\nWITH item,\n collect(DISTINCT {coll: coll_name, form: form_name, grp: group_name}) AS chains\n\nUNWIND chains AS chain\n\nRETURN \n chain.coll AS `Collection`,\n chain.form AS `→ Form`,\n chain.grp AS `→ ItemGroup`,\n item.name AS `→ Item (Selected)`\nORDER BY `Collection`, `→ Form`, `→ ItemGroup`\nLIMIT 1000", + "width": 15, + "height": 3, + "x": 0, + "y": 4, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "noDataMessage": "Select an Item and Version above", + "wrapContent": true + } + }, + { + "id": "3a3a3333-3333-3333-3333-333333333338", + "title": "Impact Summary", + "query": "MATCH (item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nWHERE item.oid = $neodash_odmitemvalue_oid_impact \n AND item_version.version = $neodash_item_version_impact\n\nOPTIONAL MATCH (group:OdmItemGroupValue)-[:ITEM_REF]->(item)\nWITH item, collect(DISTINCT group) AS groups\n\nOPTIONAL MATCH (form:OdmFormValue)-[:ITEM_GROUP_REF]->(:OdmItemGroupValue)-[:ITEM_REF]->(item)\nWITH item, groups, collect(DISTINCT form) AS forms\n\nOPTIONAL MATCH (coll:OdmStudyEventValue)-[:FORM_REF]->(:OdmFormValue)-[:ITEM_GROUP_REF]->(:OdmItemGroupValue)-[:ITEM_REF]->(item)\nWITH item, groups, forms, collect(DISTINCT coll) AS collections\n\nRETURN \n '# 🔹 Item Impact Analysis\\n\\n' +\n '## Item Information\\n' +\n '- **Item OID**: ' + item.oid + '\\n' +\n '- **Item Name**: ' + item.name + '\\n' +\n '- **Version**: ' + toString($neodash_item_version_impact) + '\\n' +\n '- **Data Type**: ' + coalesce(item.data_type, 'N/A') + '\\n\\n' +\n '---\\n\\n' +\n '## Impact Summary\\n' +\n '- **ItemGroups Using This Item**: ' + toString(size(groups)) + '\\n' +\n '- **Forms Affected**: ' + toString(size(forms)) + '\\n' +\n '- **Collections Affected**: ' + toString(size(collections)) + '\\n\\n' +\n '---\\n\\n' +\n '## ⚠️ Change Impact\\n' +\n 'Any changes to this Item will potentially affect:\\n' +\n '- ' + toString(size(groups)) + ' ItemGroup(s)\\n' +\n '- ' + toString(size(forms)) + ' Form(s)\\n' +\n '- ' + toString(size(collections)) + ' Collection(s)\\n\\n' +\n 'This represents a **' + \n CASE \n WHEN size(forms) >= 5 THEN 'HIGH'\n WHEN size(formss) >= 2 THEN 'MEDIUM'\n ELSE 'LOW'\n END + '** impact level.'\n AS summary", + "width": 8, + "height": 7, + "x": 15, + "y": 0, + "type": "value", + "selection": {}, + "settings": { + "fontSize": 14, + "monospace": true + } + } + ] + }, + { + "title": "ItemGroup Impact", + "reports": [ + { + "id": "2a2a2222-2222-2222-2222-222222222221", + "title": "Select ItemGroup", + "query": "MATCH (n:`OdmItemGroupValue`) \nWHERE toLower(toString(n.`oid`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`oid` as value, n.`name` as display ORDER BY size(toString(value)) ASC LIMIT 1000", + "width": 8, + "height": 2, + "x": 0, + "y": 0, + "type": "select", + "selection": {}, + "settings": { + "type": "Custom Query", + "refreshButtonEnabled": true, + "helperText": "Select an ItemGroup to analyze", + "entityType": "odmitemgroupvalue_oid_impact", + "parameterName": "neodash_odmitemgroupvalue_oid_impact" + } + }, + { + "id": "2a2a2222-2222-2222-2222-222222222222", + "title": "Select ItemGroup Version", + "query": "MATCH (group:OdmItemGroupValue)<-[group_version:HAS_VERSION]-(group_root:OdmItemGroupRoot)\nWHERE group.oid = $neodash_odmitemgroupvalue_oid_impact\nRETURN group_version.version as value, 'Version ' + group_version.version as display\nORDER BY group_version.version DESC", + "width": 8, + "height": 2, + "x": 0, + "y": 2, + "type": "select", + "selection": {}, + "settings": { + "type": "Custom Query", + "refreshButtonEnabled": true, + "helperText": "Select ItemGroup Version", + "entityType": "group_version_impact", + "parameterName": "neodash_group_version_impact" + } + }, + { + "id": "2a2a2222-2222-2222-2222-222222222223", + "title": "ItemGroup Details", + "query": "MATCH (group:OdmItemGroupValue)<-[group_version:HAS_VERSION]-(group_root:OdmItemGroupRoot)\nWHERE group.oid = $neodash_odmitemgroupvalue_oid_impact \n AND group_version.version = $neodash_group_version_impact\nOPTIONAL MATCH (group)-[:HAS_DESCRIPTION]->(desc:OdmDescription)\nOPTIONAL MATCH (group)-[:HAS_SDTM_DOMAIN]->(domain_context:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(domain:CTCodelistTerm)\nRETURN \n group.oid AS `ItemGroup OID`,\n group.name AS `ItemGroup Name`,\n group_version.version AS Version,\n group.repeating AS Repeating,\n domain.submission_value AS `SDTM Domain`,\n desc.text AS Description", + "width": 6, + "height": 4, + "x": 8, + "y": 0, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "transposed": true, + "refreshButtonEnabled": true, + "wrapContent": true + } + }, + { + "id": "2a2a2222-2222-2222-2222-222222222224", + "title": "Items in ItemGroup - $neodash_odmitemgroupvalue_oid_impact (v $neodash_group_version_impact)", + "query": "MATCH (group:OdmItemGroupValue)<-[group_version:HAS_VERSION]-(group_root:OdmItemGroupRoot)\nWHERE group.oid = $neodash_odmitemgroupvalue_oid_impact \n AND group_version.version = $neodash_group_version_impact\nOPTIONAL MATCH (group)-[item_ref:ITEM_REF]->(item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nOPTIONAL MATCH (item)-[:HAS_DESCRIPTION]->(item_desc:OdmDescription)\nOPTIONAL MATCH (item)-[:HAS_CODELIST_TERM]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(codelist:CTCodelistTerm)\nWITH item, item_version, item_desc, item_ref, collect(DISTINCT codelist.submission_value) AS codelists\nORDER BY item_ref.order_number\nRETURN \n item.oid AS `Item OID`,\n item.name AS `Item Name`,\n item_version.version AS Version,\n item.data_type AS `Data Type`,\n item_ref.mandatory AS Mandatory,\n item_ref.order_number AS `Order`,\n CASE WHEN size(codelists) > 0 THEN apoc.text.join(codelists, '; ') ELSE null END AS `Codelist Terms`,\n item_desc.text AS Description\nORDER BY `Order`", + "width": 22, + "height": 5, + "x": 0, + "y": 7, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "fullscreenEnabled": true, + "allowDownload": true, + "columnWidths": "[1,1.5,0.6,0.8,0.6,0.6,1.5,2]", + "noDataMessage": "Select an ItemGroup and Version above", + "wrapContent": true + } + }, + { + "id": "2a2a2222-2222-2222-2222-222222222225", + "title": "Forms using ItemGroup - $neodash_odmitemgroupvalue_oid_impact (v $neodash_group_version_impact)", + "query": "MATCH (group:OdmItemGroupValue)<-[group_version:HAS_VERSION]-(group_root:OdmItemGroupRoot)\nWHERE group.oid = $neodash_odmitemgroupvalue_oid_impact \n AND group_version.version = $neodash_group_version_impact\nOPTIONAL MATCH (form_root:OdmFormRoot)-[form_version:HAS_VERSION]->(form:OdmFormValue)-[ig_ref:ITEM_GROUP_REF]->(group)\nRETURN \n form.oid AS `Form OID`,\n form.name AS `Form Name`,\n form_version.version AS `Form Version`,\n ig_ref.mandatory AS `Mandatory in Form`,\n ig_ref.order_number AS `Order in Form`\nORDER BY `Form Name`, `Form Version` DESC", + "width": 14, + "height": 3, + "x": 0, + "y": 4, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "noDataMessage": "Select an ItemGroup and Version above", + "wrapContent": true + } + }, + { + "id": "2a2a2222-2222-2222-2222-222222222226", + "title": "Collections using ItemGroup - $neodash_odmitemgroupvalue_oid_impact (v $neodash_group_version_impact)", + "query": "MATCH (group:OdmItemGroupValue)<-[group_version:HAS_VERSION]-(group_root:OdmItemGroupRoot)\nWHERE group.oid = $neodash_odmitemgroupvalue_oid_impact \n AND group_version.version = $neodash_group_version_impact\nOPTIONAL MATCH (coll_root:OdmStudyEventRoot)-[coll_version:HAS_VERSION]->(coll:OdmStudyEventValue)-[:FORM_REF]->(form:OdmFormValue)-[:ITEM_GROUP_REF]->(group)\nRETURN DISTINCT\n coll.name AS `Collection Name`,\n coll_version.version AS `Collection Version`,\n form.name AS `Via Form`\nORDER BY `Collection Name`, `Collection Version` DESC, `Via Form`", + "width": 22, + "height": 4, + "x": 0, + "y": 12, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "noDataMessage": "Select an ItemGroup and Version above", + "wrapContent": true + } + }, + { + "id": "2a2a2222-2222-2222-2222-222222222227", + "title": "Impact Summary", + "query": "MATCH (group:OdmItemGroupValue)<-[group_version:HAS_VERSION]-(group_root:OdmItemGroupRoot)\nWHERE group.oid = $neodash_odmitemgroupvalue_oid_impact \n AND group_version.version = $neodash_group_version_impact\n\nOPTIONAL MATCH (group)-[:ITEM_REF]->(item:OdmItemValue)\nWITH group, collect(DISTINCT item) AS items\n\nOPTIONAL MATCH (form:OdmFormValue)-[:ITEM_GROUP_REF]->(group)\nWITH group, items, collect(DISTINCT form) AS forms\n\nOPTIONAL MATCH (coll:OdmStudyEventValue)-[:FORM_REF]->(:OdmFormValue)-[:ITEM_GROUP_REF]->(group)\nWITH group, items, forms, collect(DISTINCT coll) AS collections\n\nRETURN \n '# 📦 ItemGroup Impact Analysis\\n\\n' +\n '## ItemGroup Information\\n' +\n '- **ItemGroup OID**: ' + group.oid + '\\n' +\n '- **ItemGroup Name**: ' + group.name + '\\n' +\n '- **Version**: ' + toString($neodash_group_version_impact) + '\\n\\n' +\n '---\\n\\n' +\n '## Impact Summary\\n' +\n '- **Items Affected**: ' + toString(size(items)) + '\\n' +\n '- **Forms Using This ItemGroup**: ' + toString(size(forms)) + '\\n' +\n '- **Collections Affected**: ' + toString(size(collections)) + '\\n\\n' +\n '---\\n\\n' +\n '## ⚠️ Change Impact\\n' +\n 'Any changes to this ItemGroup will potentially affect:\\n' +\n '- ' + toString(size(items)) + ' Item(s)\\n' +\n '- ' + toString(size(forms)) + ' Form(s)\\n' +\n '- ' + toString(size(collections)) + ' Collection(s)\\n'\n AS summary", + "width": 8, + "height": 7, + "x": 14, + "y": 0, + "type": "value", + "selection": {}, + "settings": { + "fontSize": 14, + "monospace": true + } + } + ] + }, + { + "title": "Form Impact", + "reports": [ + { + "id": "1a1a1111-1111-1111-1111-111111111111", + "title": "Select Form", + "query": "MATCH (n:`OdmFormValue`) \nWHERE toLower(toString(n.`oid`)) CONTAINS toLower($input) \nRETURN DISTINCT n.`oid` as value, n.`name` as display ORDER BY size(toString(value)) ASC LIMIT 1000", + "width": 6, + "height": 2, + "x": 0, + "y": 0, + "type": "select", + "selection": {}, + "settings": { + "type": "Custom Query", + "refreshButtonEnabled": true, + "helperText": "Select a Form to analyze", + "entityType": "odmformvalue_oid_impact", + "parameterName": "neodash_odmformvalue_oid_impact" + } + }, + { + "id": "1a1a1111-1111-1111-1111-111111111112", + "title": "Select Form Version", + "query": "MATCH (form:OdmFormValue)<-[form_version:HAS_VERSION]-(form_root:OdmFormRoot)\nWHERE form.oid = $neodash_odmformvalue_oid_impact\nRETURN form_version.version as value, 'Version ' + form_version.version as display\nORDER BY form_version.version DESC", + "width": 6, + "height": 2, + "x": 0, + "y": 2, + "type": "select", + "selection": {}, + "settings": { + "type": "Custom Query", + "refreshButtonEnabled": true, + "helperText": "Select Form Version", + "entityType": "form_version_impact", + "parameterName": "neodash_form_version_impact" + } + }, + { + "id": "1a1a1111-1111-1111-1111-111111111113", + "title": "Form Details", + "query": "MATCH (form:OdmFormValue)<-[form_version:HAS_VERSION]-(form_root:OdmFormRoot)\nWHERE form.oid = $neodash_odmformvalue_oid_impact \n AND form_version.version = $neodash_form_version_impact\nOPTIONAL MATCH (form)-[:HAS_DESCRIPTION]->(desc:OdmDescription)\nRETURN \n form.oid AS `Form OID`,\n form.name AS `Form Name`,\n form_version.version AS Version,\n form.repeating AS Repeating,\n desc.text AS Description", + "width": 8, + "height": 4, + "x": 6, + "y": 0, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "transposed": true, + "refreshButtonEnabled": true, + "wrapContent": true + } + }, + { + "id": "1a1a1111-1111-1111-1111-111111111114", + "title": "ItemGroups in Form - $neodash_odmformvalue_oid_impact (v $neodash_form_version_impact)", + "query": "MATCH (form:OdmFormValue)<-[form_version:HAS_VERSION]-(form_root:OdmFormRoot)\nWHERE form.oid = $neodash_odmformvalue_oid_impact \n AND form_version.version = $neodash_form_version_impact\nOPTIONAL MATCH (form)-[ig_ref:ITEM_GROUP_REF]->(group:OdmItemGroupValue)<-[group_version:HAS_VERSION]-(group_root:OdmItemGroupRoot)\nOPTIONAL MATCH (group)-[:HAS_DESCRIPTION]->(group_desc:OdmDescription)\nWITH form, group, group_version, group_desc, ig_ref\nORDER BY ig_ref.order_number\nRETURN \n group.oid AS `ItemGroup OID`,\n group.name AS `ItemGroup Name`,\n group_version.version AS Version,\n group.repeating AS Repeating,\n ig_ref.order_number AS `Order`,\n ig_ref.mandatory AS Mandatory,\n group_desc.text AS Description\nORDER BY `Order`", + "width": 14, + "height": 3, + "x": 0, + "y": 4, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "fullscreenEnabled": true, + "allowDownload": true, + "noDataMessage": "Select a Form and Version above", + "wrapContent": true + } + }, + { + "id": "1a1a1111-1111-1111-1111-111111111115", + "title": "All Items in Form - $neodash_odmformvalue_oid_impact (v $neodash_form_version_impact)", + "query": "MATCH (form:OdmFormValue)<-[form_version:HAS_VERSION]-(form_root:OdmFormRoot)\nWHERE form.oid = $neodash_odmformvalue_oid_impact \n AND form_version.version = $neodash_form_version_impact\nOPTIONAL MATCH (form)-[ig_ref:ITEM_GROUP_REF]->(group:OdmItemGroupValue)-[item_ref:ITEM_REF]->(item:OdmItemValue)<-[item_version:HAS_VERSION]-(item_root:OdmItemRoot)\nOPTIONAL MATCH (item)-[:HAS_DESCRIPTION]->(item_desc:OdmDescription)\nWITH form, group, ig_ref, item, item_version, item_desc, item_ref\nORDER BY ig_ref.order_number, item_ref.order_number\nRETURN \n group.oid AS `ItemGroup OID`,\n group.name AS `ItemGroup Name`,\n item.oid AS `Item OID`,\n item.name AS `Item Name`,\n item_version.version AS `Item Version`,\n item.data_type AS `Data Type`,\n item_ref.mandatory AS Mandatory,\n item_ref.order_number AS `Order in Group`,\n item_desc.text AS Description\nORDER BY ig_ref.order_number, `Order in Group`", + "width": 21, + "height": 4, + "x": 0, + "y": 7, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "fullscreenEnabled": true, + "allowDownload": true, + "columnWidths": "[1,1.5,1,1.5,0.8,0.8,0.6,0.8,2]", + "noDataMessage": "Select a Form and Version above", + "wrapContent": true + } + }, + { + "id": "1a1a1111-1111-1111-1111-111111111116", + "title": "Collections using Form - $neodash_odmformvalue_oid_impact (v $neodash_form_version_impact)", + "query": "MATCH (form_root:OdmFormRoot)-[form_version:HAS_VERSION]->(form:OdmFormValue)\nWHERE form.oid = $neodash_odmformvalue_oid_impact \n AND form_version.version = $neodash_form_version_impact\nOPTIONAL MATCH (coll_root:OdmStudyEventRoot)-[coll_version:HAS_VERSION]->(coll:OdmStudyEventValue)-[:FORM_REF]->(form)\nRETURN \n coll.name AS `Collection Name`,\n coll_version.version AS `Collection Version`,\n coll.oid AS `Collection OID`\nORDER BY `Collection Name`, `Collection Version` DESC", + "width": 21, + "height": 4, + "x": 0, + "y": 11, + "type": "table", + "selection": {}, + "settings": { + "compact": true, + "refreshButtonEnabled": true, + "noDataMessage": "Select a Form and Version above", + "wrapContent": true + } + }, + { + "id": "1a1a1111-1111-1111-1111-111111111117", + "title": "Impact Summary", + "query": "MATCH (form:OdmFormValue)<-[form_version:HAS_VERSION]-(form_root:OdmFormRoot)\nWHERE form.oid = $neodash_odmformvalue_oid_impact \n AND form_version.version = $neodash_form_version_impact\n\nOPTIONAL MATCH (form)-[:ITEM_GROUP_REF]->(group:OdmItemGroupValue)\nWITH form, collect(DISTINCT group) AS groups\n\nOPTIONAL MATCH (form)-[:ITEM_GROUP_REF]->(:OdmItemGroupValue)-[:ITEM_REF]->(item:OdmItemValue)\nWITH form, groups, collect(DISTINCT item) AS items\n\nOPTIONAL MATCH (coll:OdmStudyEventValue)-[:FORM_REF]->(form)\nWITH form, groups, items, collect(DISTINCT coll) AS collections\n\nRETURN \n '# 📋 Form Impact Analysis\\n\\n' +\n '## Form Information\\n' +\n '- **Form OID**: ' + form.oid + '\\n' +\n '- **Form Name**: ' + form.name + '\\n' +\n '- **Version**: ' + toString($neodash_form_version_impact) + '\\n\\n' +\n '---\\n\\n' +\n '## Impact Summary\\n' +\n '- **ItemGroups Affected**: ' + toString(size(groups)) + '\\n' +\n '- **Items Affected**: ' + toString(size(items)) + '\\n' +\n '- **Collections Using This Form**: ' + toString(size(collections)) + '\\n\\n' +\n '---\\n\\n' +\n '## ⚠️ Change Impact\\n' +\n 'Any changes to this Form will potentially affect:\\n' +\n '- ' + toString(size(groups)) + ' ItemGroup(s)\\n' +\n '- ' + toString(size(items)) + ' Item(s)\\n' +\n '- ' + toString(size(collections)) + ' Collection(s)\\n'\n AS summary", + "width": 7, + "height": 7, + "x": 14, + "y": 0, + "type": "value", + "selection": {}, + "settings": { + "fontSize": 14, + "monospace": true + } + } + ] + } + ], + "parameters": {}, + "extensions": { + "active": true, + "activeReducers": [], + "advanced-charts": { + "active": true + }, + "styling": { + "active": true + }, + "actions": { + "active": true + } + }, + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" +} \ No newline at end of file diff --git a/neo4j-mdr-db/neodash/neodash_reports/laboratory_data_specification.json b/neo4j-mdr-db/neodash/neodash_reports/laboratory_data_specification.json index 0822767d..c32e7658 100644 --- a/neo4j-mdr-db/neodash/neodash_reports/laboratory_data_specification.json +++ b/neo4j-mdr-db/neodash/neodash_reports/laboratory_data_specification.json @@ -163,7 +163,7 @@ { "id": "63faa4fe-04b0-4f6a-be95-c765df55d6af", "title": "Visits - $neodash_study_name", - "query": "MATCH\n (n:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)-->\n (visit:StudyVisit)-[r2:HAS_VISIT_NAME]->(n3:VisitNameRoot)-->(n4:VisitNameValue) \nWHERE \n // Show visits NOT in the exclusion list (default behavior)\n NOT toLower(visit.visit_class) IN ['non_visit', 'unscheduled_visit']\n OR\n // OR include excluded visits if explicitly specified in parameter\n (size($neodash_visit_class) > 0 AND visit.visit_class IN $neodash_visit_class)\nOPTIONAL MATCH(visit)-->\n (s_act_sch:StudyActivitySchedule)<--\n (sact:StudyActivity)-->\n (:StudyActivitySubGroup)-->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (agv:ActivityGroupValue),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping),(sact)-->(act:ActivityValue)\nWHERE \n // LAB: Laboratory Assessments excluding Antibodies and PK Sampling\n (('LAB' IN $neodash_spec_type AND agv.name = \"Laboratory Assessments\" AND asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\")\n OR\n // PK: PK/PD related groups\n ('PK' IN $neodash_spec_type AND (\n toLower(agv.name) IN ['pk sampling', 'pk parameters', 'pd sampling'] \n OR (toLower(agv.name) = 'laboratory assessments' AND toLower(asgv.name) = 'pharmacodynamics')\n ))\n OR\n // AB: Antibodies only\n ('AB' IN $neodash_spec_type AND asgv.name = \"Antibodies\"))\n AND NOT (s_act_sch)<--(:Delete)\nWITH DISTINCT n4, visit, sact WHERE NOT (NOT visit.visit_class IN $neodash_visit_class AND sact.uid IS NULL)\nWITH DISTINCT\n visit,\n CASE WHEN visit.visit_class IN $neodash_visit_class THEN apoc.text.capitalize(toLower(replace(visit.visit_class, '_', ' '))) ELSE n4.name END AS `Visit label in protocol`,\n \" \" AS `Visit label in supplier database`,\n CASE WHEN visit.visit_class IN $neodash_visit_class THEN CASE WHEN visit.visit_class = 'UNSCHEDULED_VISIT' THEN split(visit.visit_class, \"_\")[0] ELSE replace(toUpper(visit.visit_class),'_','-') END ELSE visit.short_visit_label END AS `Visit in data file`,\n toInteger(visit.unique_visit_number) AS visit_order,\n visit.visit_number AS visit_number\nORDER BY visit_order\nRETURN DISTINCT\n `Visit label in protocol`,\n `Visit label in supplier database`,\n `Visit in data file`", + "query": "MATCH\n (n:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)-->\n (visit:StudyVisit)-[r2:HAS_VISIT_NAME]->(n3:VisitNameRoot)-->(n4:VisitNameValue)\nWHERE \n // Show visits NOT in the exclusion list (default behavior)\n NOT toLower(visit.visit_class) IN ['non_visit', 'unscheduled_visit']\n OR\n // OR include excluded visits if explicitly specified in parameter\n (size($neodash_visit_class) > 0 AND visit.visit_class IN $neodash_visit_class)\nOPTIONAL MATCH(visit)-->\n (s_act_sch:StudyActivitySchedule)<--\n (sact:StudyActivity)-->\n (:StudyActivitySubGroup)-->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (agv:ActivityGroupValue)\nWHERE \n // LAB: Laboratory Assessments excluding Antibodies and PK Sampling\n (('LAB' IN $neodash_spec_type AND agv.name = \"Laboratory Assessments\" AND asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\")\n OR\n // PK: PK/PD related groups\n ('PK' IN $neodash_spec_type AND (\n toLower(agv.name) IN ['pk sampling', 'pk parameter', 'pd sampling'] \n OR (toLower(agv.name) = 'laboratory assessments' AND toLower(asgv.name) = 'pharmacodynamics')\n ))\n OR\n // AB: Antibodies only\n ('AB' IN $neodash_spec_type AND asgv.name = \"Antibodies\"))\nAND NOT (s_act_sch)<--(:Delete)\nWITH DISTINCT n4, visit, sact WHERE NOT (NOT visit.visit_class IN $neodash_visit_class AND sact.uid IS NULL)\nWITH DISTINCT\n visit,\n CASE WHEN visit.visit_class IN $neodash_visit_class THEN apoc.text.capitalize(toLower(replace(visit.visit_class, '_', ' '))) ELSE n4.name END AS `Visit label in protocol`,\n \" \" AS `Visit label in supplier database`,\n CASE WHEN visit.visit_class IN $neodash_visit_class THEN CASE WHEN visit.visit_class = 'UNSCHEDULED_VISIT' THEN split(visit.visit_class, \"_\")[0] ELSE replace(toUpper(visit.visit_class),'_','-') END ELSE visit.short_visit_label END AS `Visit in data file`,\n toInteger(visit.unique_visit_number) AS visit_order,\n visit.visit_number AS visit_number\nORDER BY visit_order\nRETURN DISTINCT\n `Visit label in protocol`,\n `Visit label in supplier database`,\n `Visit in data file`", "width": 15, "height": 10, "x": 0, @@ -274,7 +274,7 @@ { "id": "05474e92-e761-4dff-b9d1-844ee119147f", "title": "LAB Content - $neodash_study_name -TABLE 1: REMOVE unwanted lab assessments by selecting in the dropdown to the right", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (:ActivityGroupValue {name: \"Laboratory Assessments\"}),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWHERE asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\" AND asgv.name <> \"Pharmacodynamics\"\nWITH DISTINCT s, s_act\nMATCH\n(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n CASE WHEN ai.topic_code in $neodash_remove_activity_instances THEN '❌' ELSE '✅' END AS `In/Excluded`,\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS LBSPEC,\n ai.topic_code AS TOPICCD,\n lbstresc AS `Valid LBSTRESC values for categorical results`,\n lbmethod AS LBMETHOD,\n ' ' AS LBANMETH,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (:ActivityGroupValue {name: \"Laboratory Assessments\"})\nWHERE asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\" AND asgv.name <> \"Pharmacodynamics\"\nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n CASE WHEN ai.topic_code in $neodash_remove_activity_instances THEN '❌' ELSE '✅' END AS `In/Excluded`,\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS LBSPEC,\n ai.topic_code AS TOPICCD,\n lbstresc AS `Valid LBSTRESC values for categorical results`,\n lbmethod AS LBMETHOD,\n ' ' AS LBANMETH,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`", "width": 19, "height": 7, "x": 0, @@ -395,7 +395,7 @@ { "id": "0b18aa27-5d0e-4e35-be9a-ffe9b5104e16", "title": "LAB Content - $neodash_study_name -TABLE 2 : The final list of assessments that can be exported", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (:ActivityGroupValue {name: \"Laboratory Assessments\"}),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWHERE asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\" AND asgv.name <> \"Pharmacodynamics\"\nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai WHERE size($neodash_remove_activity_instances ) = 0 OR NOT ai.topic_code IN $neodash_remove_activity_instances\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS LBSPEC,\n ai.topic_code AS TOPICCD,\n lbstresc AS `Valid LBSTRESC values for categorical results`,\n lbmethod AS LBMETHOD,\n ' ' AS LBANMETH,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n\n", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (:ActivityGroupValue {name: \"Laboratory Assessments\"})\nWHERE asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\" AND asgv.name <> \"Pharmacodynamics\"\nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai WHERE size($neodash_remove_activity_instances ) = 0 OR NOT ai.topic_code IN $neodash_remove_activity_instances\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS LBSPEC,\n ai.topic_code AS TOPICCD,\n lbstresc AS `Valid LBSTRESC values for categorical results`,\n lbmethod AS LBMETHOD,\n ' ' AS LBANMETH,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n\n", "width": 19, "height": 5, "x": 0, @@ -521,7 +521,7 @@ { "id": "ceec002d-fb89-4d61-90e9-336d10d0cf53", "title": "Select assessments to REMOVE", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (:ActivityGroupValue {name: \"Laboratory Assessments\"}),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWHERE asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\" AND asgv.name <> \"Pharmacodynamics\"\nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (n:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWHERE toLower(toString(n.topic_code)) CONTAINS toLower($input) \nRETURN DISTINCT n.topic_code as value, n.topic_code as display ORDER BY size(toString(value)) ASC LIMIT 100", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (:ActivityGroupValue {name: \"Laboratory Assessments\"})\nWHERE asgv.name <> \"Antibodies\" AND asgv.name <> \"PK Sampling\" AND asgv.name <> \"Pharmacodynamics\"\nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (n:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWHERE toLower(toString(n.topic_code)) CONTAINS toLower($input) \nRETURN DISTINCT n.topic_code as value, n.topic_code as display ORDER BY size(toString(value)) ASC LIMIT 100", "width": 5, "height": 2, "x": 19, @@ -548,7 +548,7 @@ { "id": "05474e92-e761-4dff-b9d1-844ee119147f", "title": "PK Content - $neodash_study_name -TABLE 1: REMOVE unwanted PK assessments by selecting in the dropdown to the right", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (agv:ActivityGroupValue),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWITH DISTINCT s, s_act, agv, asgv where (toLower(agv.name) in ['pk sampling','pk parameters']) OR (toLower(agv.name) in ['pd sampling']) OR (toLower(agv.name)='laboratory assessments' AND toLower(asgv.name)='pharmacodynamics') \nWITH DISTINCT s, s_act \nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n CASE WHEN ai.topic_code in $neodash_remove_pk_activity_instances THEN '❌' ELSE '✅' END AS `In/Excluded`,\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS PCSPEC,\nai.topic_code AS TOPICCD,\n lbmethod as PCMETHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (agv:ActivityGroupValue)\nWITH DISTINCT s, s_act, agv, asgv where (toLower(agv.name) in ['pk sampling','pk parameters']) OR (toLower(agv.name) in ['pd sampling']) OR (toLower(agv.name)='laboratory assessments' AND toLower(asgv.name)='pharmacodynamics') \nWITH DISTINCT s, s_act \nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n CASE WHEN ai.topic_code in $neodash_remove_pk_activity_instances THEN '❌' ELSE '✅' END AS `In/Excluded`,\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS PCSPEC,\nai.topic_code AS TOPICCD,\n lbmethod as PCMETHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n", "width": 19, "height": 7, "x": 0, @@ -655,7 +655,7 @@ { "id": "0b18aa27-5d0e-4e35-be9a-ffe9b5104e16", "title": "PK Content - $neodash_study_name -TABLE 2 : The final list of assessments that can be exported", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (agv:ActivityGroupValue),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWITH DISTINCT s, s_act, agv, asgv where (toLower(agv.name) in ['pk sampling','pk parameters']) OR (toLower(agv.name) in ['pd sampling']) OR (toLower(agv.name)='laboratory assessments' AND toLower(asgv.name)='pharmacodynamics') \nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai WHERE size($neodash_remove_pk_activity_instances) = 0 OR NOT ai.topic_code IN $neodash_remove_pk_activity_instances\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS PCSPEC,\nai.topic_code AS TOPICCD,\n lbmethod as PCMETHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n\n", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (agv:ActivityGroupValue)\nWITH DISTINCT s, s_act, agv, asgv where (toLower(agv.name) in ['pk sampling','pk parameters']) OR (toLower(agv.name) in ['pd sampling']) OR (toLower(agv.name)='laboratory assessments' AND toLower(asgv.name)='pharmacodynamics') \nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai WHERE size($neodash_remove_pk_activity_instances) = 0 OR NOT ai.topic_code IN $neodash_remove_pk_activity_instances\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS PCSPEC,\nai.topic_code AS TOPICCD,\n lbmethod as PCMETHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n\n", "width": 19, "height": 5, "x": 0, @@ -767,7 +767,7 @@ { "id": "ceec002d-fb89-4d61-90e9-336d10d0cf53", "title": "Select assessments to REMOVE", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (agv:ActivityGroupValue),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWITH DISTINCT s, s_act, agv, asgv where (toLower(agv.name) in ['pk sampling','pk parameters']) OR (toLower(agv.name) in ['pd sampling']) OR (toLower(agv.name)='laboratory assessments' AND toLower(asgv.name)='pharmacodynamics') \nWITH DISTINCT s, s_act\nMATCH\n(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH(n:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWHERE toLower(toString(n.topic_code)) CONTAINS toLower($input) \nRETURN DISTINCT n.topic_code as value, n.topic_code as display ORDER BY size(toString(value)) ASC LIMIT 100", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (agv:ActivityGroupValue)\nWITH DISTINCT s, s_act, agv, asgv where (toLower(agv.name) in ['pk sampling','pk parameters']) OR (toLower(agv.name) in ['pd sampling']) OR (toLower(agv.name)='laboratory assessments' AND toLower(asgv.name)='pharmacodynamics') \nWITH DISTINCT s, s_act\nMATCH (s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH(n:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWHERE toLower(toString(n.topic_code)) CONTAINS toLower($input) \nRETURN DISTINCT n.topic_code as value, n.topic_code as display ORDER BY size(toString(value)) ASC LIMIT 100", "width": 5, "height": 2, "x": 19, @@ -795,7 +795,7 @@ { "id": "05474e92-e761-4dff-b9d1-844ee119147f", "title": "AB Content - $neodash_study_name -TABLE 1: REMOVE unwanted lab assessments by selecting in the dropdown to the right", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (:ActivityGroupValue),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWHERE asgv.name = \"Antibodies\"\nWITH DISTINCT s, s_act\nMATCH(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n \n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n CASE WHEN ai.topic_code in $neodash_remove_ab_activity_instances THEN '❌' ELSE '✅' END AS `In/Excluded`,\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS SPEC,\n ai.topic_code AS TOPICCD,\n lbstresc as `If result is categorical, specify valid ORRES values from categorical response list`,\n lbmethod AS METHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (:ActivityGroupValue)\nWHERE asgv.name = \"Antibodies\"\nWITH DISTINCT s, s_act\nMATCH(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n CASE WHEN ai.topic_code in $neodash_remove_ab_activity_instances THEN '❌' ELSE '✅' END AS `In/Excluded`,\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS SPEC,\n ai.topic_code AS TOPICCD,\n lbstresc as `If result is categorical, specify valid ORRES values from categorical response list`,\n lbmethod AS METHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n", "width": 19, "height": 7, "x": 0, @@ -909,7 +909,7 @@ { "id": "0b18aa27-5d0e-4e35-be9a-ffe9b5104e16", "title": "AB Content - $neodash_study_name -TABLE 2 : The final list of assessments that can be exported", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (:ActivityGroupValue),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWHERE asgv.name = \"Antibodies\"\nWITH DISTINCT s, s_act\nMATCH(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai WHERE size($neodash_remove_ab_activity_instances ) = 0 OR NOT ai.topic_code IN $neodash_remove_ab_activity_instances\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS SPEC,\n ai.topic_code AS TOPICCD,\n lbstresc as `If result is categorical, specify valid ORRES values from categorical response list`,\n lbmethod AS METHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n\n\n", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (:ActivityGroupValue)\nWHERE asgv.name = \"Antibodies\"\nWITH DISTINCT s, s_act\nMATCH(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH (ai:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWITH DISTINCT s, act, ai WHERE size($neodash_remove_ab_activity_instances ) = 0 OR NOT ai.topic_code IN $neodash_remove_ab_activity_instances\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH (ai)-[:CONTAINS_ACTIVITY_ITEM]->(aitm1:ActivityItem)\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (p_unitdef:UnitDefinitionValue)-[:HAS_CT_DIMENSION]->(context:CTTermContext)\n with context\n MATCH(context)<-[:HAS_CT_DIMENSION]-(possible_units:UnitDefinitionValue)-[r1:HAS_CT_UNIT]->(x:CTTermContext)-[r2:HAS_SELECTED_TERM]-> (y:CTTermRoot)<-[r3:HAS_TERM_ROOT]-(cdisc_unit:CTCodelistTerm),\n (x)-[:HAS_SELECTED_CODELIST]->(:CTCodelistRoot)-[:HAS_TERM]->(cdisc_unit)\n WITH distinct x, possible_units.name as pos_unit, CASE WHEN possible_units.convertible_unit THEN '*' ELSE '' END as convertible, cdisc_unit.submission_value as sub_vals \n WITH x, apoc.text.join(collect(pos_unit+convertible),';') as pos_units, sub_vals\n return apoc.text.join(collect(pos_units+'['+sub_vals+']'),\" | \") as possible_units\n}\nCALL {\n WITH s, act, ai\nOPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"standard_unit\"})\nWITH DISTINCT aitm1\nOPTIONAL MATCH\n (aitm1)-[:HAS_UNIT_DEFINITION]->\n (:UnitDefinitionRoot)-[:LATEST]->\n (std_unit:UnitDefinitionValue)-[:HAS_CT_UNIT]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(cdisc_std_unit:CTCodelistTerm)\nRETURN std_unit, cdisc_std_unit\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm1:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"specimen\"})\n WITH DISTINCT aitm1\n OPTIONAL MATCH\n (aitm1)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val1:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val1.submission_value) AS lbspec\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm2:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"method\"})\n WITH DISTINCT aitm2\n OPTIONAL MATCH\n (aitm2)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val2:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val2.submission_value) AS lbmethod\n}\nCALL {\n WITH s, act, ai\n OPTIONAL MATCH\n (ai)-[:CONTAINS_ACTIVITY_ITEM]->\n (aitm3:ActivityItem)<--\n (:ActivityItemClassRoot)-[:LATEST]->\n (:ActivityItemClassValue {name: \"original_result\"})\n WITH DISTINCT aitm3\n OPTIONAL MATCH\n (aitm3)-[:HAS_CT_TERM]->\n (:CTTermContext)-[:HAS_SELECTED_TERM]->\n (:CTTermRoot)<-[:HAS_TERM_ROOT]-(subm_val3:CTCodelistTerm)\n RETURN collect(DISTINCT subm_val3.submission_value) AS lbstresc\n}\nWITH\n act,\n ai,\n CASE\n WHEN size(lbmethod) > 0 THEN apoc.text.join(lbmethod,\";\")\n ELSE ' '\n END AS lbmethod,\n CASE\n WHEN size(lbspec) > 0 THEN apoc.text.join(lbspec,\";\") \n ELSE ' '\n END AS lbspec,\n CASE\n WHEN possible_units='' THEN ' '\n ELSE possible_units\n END AS possible_units,\n CASE\n WHEN size(lbstresc) > 0 THEN apoc.text.join(lbstresc,\";\") \n ELSE ' '\n END AS lbstresc,\n CASE WHEN std_unit.name is null THEN ' ' ELSE std_unit.name END AS `Standard unit`\n RETURN DISTINCT\n act.name AS `Assessment description based on protocol`,\n ' ' AS `Supplier Assessment`,\n ' ' AS `Supplier Unit`,\n ' ' AS `Supplier Method Name`,\n ' ' as UNITCOLL,\n lbspec AS SPEC,\n ai.topic_code AS TOPICCD,\n lbstresc as `If result is categorical, specify valid ORRES values from categorical response list`,\n lbmethod AS METHOD,\n`Standard unit`,\npossible_units as `Units in units dimension[CDISC Submission Value]`\n\n\n", "width": 19, "height": 5, "x": 0, @@ -1028,7 +1028,7 @@ { "id": "ceec002d-fb89-4d61-90e9-336d10d0cf53", "title": "Select assessments to REMOVE", - "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)-[:HAS_GROUP]->\n (avg:ActivityValidGroup)-[:IN_GROUP]->\n (:ActivityGroupValue),\n (avg)<-[:IN_SUBGROUP]-(:ActivityGrouping)\nWHERE asgv.name = \"Antibodies\"\nWITH DISTINCT s, s_act\nMATCH\n(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH(n:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWHERE toLower(toString(n.topic_code)) CONTAINS toLower($input) \nRETURN DISTINCT n.topic_code as value, n.topic_code as display ORDER BY size(toString(value)) ASC LIMIT 100", + "query": "MATCH\n (:StudyRoot {uid: $neodash_studyroot_uid})-[:LATEST]->\n (s:StudyValue)\nWITH s\nMATCH(s)-->\n (s_act:StudyActivity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (:StudyActivitySubGroup)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (asgv:ActivitySubGroupValue)<-[:HAS_SELECTED_SUBGROUP]-\n (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->\n (:ActivityGroupValue)\nWHERE asgv.name = \"Antibodies\"\nWITH DISTINCT s, s_act\nMATCH\n(s)-[:HAS_STUDY_ACTIVITY]->(s_act)-[:HAS_SELECTED_ACTIVITY]->(act:ActivityValue),\n(s_act)\nMATCH(s_act)-[:STUDY_ACTIVITY_HAS_SCHEDULE]->(s_act_sch:StudyActivitySchedule)<-[:STUDY_VISIT_HAS_SCHEDULE]-(visit:StudyVisit)<--(s)\nwhere not (s_act_sch)<--(:Delete)\nMATCH(n:ActivityInstanceValue)<-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (s_ai:StudyActivityInstance)<-[:HAS_STUDY_ACTIVITY_INSTANCE]-\n (s),(s_act)-->(s_ai)\nWHERE toLower(toString(n.topic_code)) CONTAINS toLower($input) \nRETURN DISTINCT n.topic_code as value, n.topic_code as display ORDER BY size(toString(value)) ASC LIMIT 100", "width": 5, "height": 2, "x": 19, diff --git a/neo4j-mdr-db/neodash/neodash_reports/study_metadata_compare.json b/neo4j-mdr-db/neodash/neodash_reports/study_metadata_compare.json index c3b722f9..4f57fcd6 100644 --- a/neo4j-mdr-db/neodash/neodash_reports/study_metadata_compare.json +++ b/neo4j-mdr-db/neodash/neodash_reports/study_metadata_compare.json @@ -28,7 +28,8 @@ "On" ] }, - "theme": "light" + "theme": "light", + "disableRowLimiting": true }, "pages": [ { @@ -461,8 +462,7 @@ "y": 3, "type": "gantt", "selection": { - "Visit ": "(label)", - "FOLLOWS ": "(label)" + "Visit ": "(label)" }, "settings": { "refreshButtonEnabled": true, @@ -507,7 +507,7 @@ { "id": "391cb06a-924b-4a56-b5fd-0e12a4429340", "title": "Planned Collections at Visits", - "query": "match(s:StudyValue) where id(s) in[toInteger($neodash_studya),toInteger($neodash_studyb)]\nwith s, id(s) as sid, CASE when id(s)=toInteger($neodash_studya) then 'Base' ELSE 'Compare' END as study\noptional match(s)-[:HAS_STUDY_VISIT]->(vis:StudyVisit)\noptional match(vis)-[:HAS_VISIT_NAME]->(vis_name_root)-[:HAS_VERSION]->(vis_name:VisitNameValue)\noptional match(vis)-[:STUDY_VISIT_HAS_SCHEDULE]->(schedule:StudyActivitySchedule)\noptional match (s)-[:HAS_STUDY_ACTIVITY_SCHEDULE]->(schedule),\n(schedule)<-[:STUDY_ACTIVITY_HAS_SCHEDULE]-(act:StudyActivity),\n(act)-[:HAS_SELECTED_ACTIVITY]->(act_val:ActivityValue),(act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_INSTANCE]->(ai_s:StudyActivityInstance)-[:HAS_SELECTED_ACTIVITY_INSTANCE]-(ai:ActivityInstanceValue),\n(ai)-[r1]->(g)<-[r2]-(act_val)\nwith distinct vis.visit_number as `Visit ID`,vis.short_visit_label as `Visit Short Label`, act_val.name as Activity, apoc.map.fromPairs(collect([study,'x'])) as map\nwhere Activity is not null\nwith `Visit ID`,`Visit Short Label`,Activity, map['Base'] as `Base - collection`, map['Compare'] as `Compare - collection` order by `Visit ID`, Activity\nwith `Visit ID`,`Visit Short Label`,Activity,`Base - collection`,`Compare - collection`, CASE WHEN `Base - collection`='x' and `Compare - collection` is null then 'Added' ELSE CASE WHEN `Base - collection` is null and `Compare - collection`='x' THEN 'Deleted' ELSE 'No change' END END as `Change Type`\nreturn `Visit ID`,`Visit Short Label`,Activity,`Base - collection`,`Compare - collection`, `Change Type`", + "query": "MATCH (sr:StudyRoot)-[:HAS_VERSION]->(s:StudyValue)\nWHERE id(s) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nWITH\n sr,\n s,\n id(s) AS sid,\n CASE\n WHEN id(s) = toInteger($neodash_studya) THEN 'Base'\n ELSE 'Compare'\n END AS study\nOPTIONAL MATCH\n (s)-[:HAS_STUDY_ACTIVITY]->\n (act)-[:HAS_SELECTED_ACTIVITY]->\n (act_val:ActivityValue)\nOPTIONAL MATCH\n (s)-[:HAS_STUDY_ACTIVITY_SCHEDULE]->\n (schedule)<-[:STUDY_ACTIVITY_HAS_SCHEDULE]-\n (act:StudyActivity)\nOPTIONAL MATCH\n (s)-->\n (act)<-[:AFTER]-\n (action:StudyAction)-[:BEFORE]->\n (act_prev)<--\n (sx:StudyValue),\n (act_prev)-->(prev_act_val:ActivityValue)\nWHERE id(sx) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nOPTIONAL MATCH (s)-[:HAS_STUDY_VISIT]->(vis:StudyVisit)\nOPTIONAL MATCH\n (vis)-[:HAS_VISIT_NAME]->\n (vis_name_root)-[:HAS_VERSION]->\n (vis_name:VisitNameValue)\nOPTIONAL MATCH\n (vis)-[:STUDY_VISIT_HAS_SCHEDULE]->(schedule:StudyActivitySchedule)\nWITH DISTINCT\n vis.visit_number AS `Visit ID`,\n vis.short_visit_label AS `Visit Short Label`,\n prev_act_val.name as `Previous Activity`,\n act_val.name AS Activity,\n apoc.map.fromPairs(collect([study, 'x'])) AS map\nWHERE Activity IS NOT NULL\nWITH\n `Visit ID`,\n `Visit Short Label`,\n Activity,\n `Previous Activity`,\n map['Base'] AS `Base - collection`,\n map['Compare'] AS `Compare - collection`\nORDER BY `Visit ID`, Activity\nWITH\n `Visit ID`,\n `Visit Short Label`,\n Activity,\n `Previous Activity`,\n `Base - collection`,\n `Compare - collection`,\n CASE\n WHEN\n `Base - collection` = 'x' AND `Compare - collection` IS NULL\n THEN \n CASE \n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Added'\n END\n ELSE\n CASE\n WHEN\n `Base - collection` IS NULL AND `Compare - collection` = 'x'\n THEN \n CASE \n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Deleted'\n END\n ELSE \n CASE \n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'No change'\n END\n END\n END AS `Change Type`\nWHERE\n $neodash_differences_only = 'No' OR `Change Type` <> 'No change'\nRETURN DISTINCT\n `Visit ID`,\n `Visit Short Label`,\n Activity,\n `Previous Activity`,\n `Base - collection`,\n `Compare - collection`,\n `Change Type`", "width": 24, "height": 4, "x": 0, @@ -539,6 +539,13 @@ "value": "No change", "customization": "row color", "customizationValue": "#5FE8866E" + }, + { + "field": "Change Type", + "condition": "=", + "value": "Replaced", + "customization": "row color", + "customizationValue": "#CDA5E84D" } ], "fullscreenEnabled": true, @@ -564,7 +571,7 @@ { "id": "b92da453-42d4-4afd-bc74-b292775def5f", "title": "Change Type", - "query": "with [\"Added\", \"No Change\", \"Deleted\"] as list\nunwind(list) as Status\nreturn Status", + "query": "with [\"Added\", \"No Change\", \"Deleted\",\"Replaced\"] as list\nunwind(list) as Status\nreturn Status", "width": 6, "height": 3, "x": 0, @@ -594,8 +601,17 @@ "value": "Deleted", "customization": "row color", "customizationValue": "#FF11116E" + }, + { + "field": "Status", + "condition": "=", + "value": "Replaced", + "customization": "row color", + "customizationValue": "#CDA5E84D" } - ] + ], + "wrapContent": true, + "compact": true } } ] @@ -606,7 +622,7 @@ { "id": "6e9939bc-8d0b-47d2-9528-8dca019895bb", "title": "Detailed Flowchart Compare Between Base and Compare Study", - "query": "//Activites compare\nMATCH (sr:StudyRoot)-[r0:HAS_VERSION]->(s:StudyValue)\nWHERE id(s) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nWITH sr, s, r0.start_date as start_date\nWITH \n CASE\n WHEN count(distinct sr) > 1 THEN 'Across Study'\n ELSE 'Within Study'\n END AS compare_type,\n apoc.map.fromPairs(\n collect(\n CASE\n WHEN id(s) = toInteger($neodash_studya) THEN ['Base', start_date]\n ELSE ['Compare', start_date]\n END\n )\n ) AS version_dates\nWITH compare_type,version_dates\nMATCH (:StudyRoot)-[r0:HAS_VERSION]->(s:StudyValue)\nWHERE id(s) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nWITH\ncompare_type,\nversion_dates,\n r0,\n s,\n id(s) AS sid,\n CASE\n WHEN id(s) = toInteger($neodash_studya) THEN 'Base'\n ELSE 'Compare'\n END AS study\nOPTIONAL MATCH\n (s)-[r3:HAS_STUDY_ACTIVITY]->\n (act)-[:HAS_SELECTED_ACTIVITY]->\n (act_val:ActivityValue)\nOPTIONAL MATCH\n (s)-[:HAS_STUDY_ACTIVITY_SCHEDULE]->(schedule),\n (schedule)<-[:STUDY_ACTIVITY_HAS_SCHEDULE]-(act:StudyActivity),\n (act)-[:HAS_SELECTED_ACTIVITY]->(act_val:ActivityValue),\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_INSTANCE]->\n (ai_s:StudyActivityInstance)-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (ai:ActivityInstanceValue),\n (ai)-[r1]->(g)<-[r2]-(act_val)\nWITH version_dates, compare_type, study, act, act_val\nOPTIONAL MATCH\n (act)-[r3:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->\n (x)-[:HAS_FLOWCHART_GROUP]->\n (ct_root:CTTermRoot)-[:HAS_NAME_ROOT]->\n (ct_name_root:CTTermNameRoot)-[:LATEST]->\n (flowchart_grp:CTTermNameValue)\nOPTIONAL MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->\n (s_act_grp)-[:HAS_SELECTED_ACTIVITY_GROUP]->\n (group:ActivityGroupValue)\nOPTIONAL MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->\n (s_act_s_grp)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (s_group:ActivitySubGroupValue)\nWITH\n version_dates,\n compare_type,\n study,\n flowchart_grp.name AS flowchart_grp,\n group.name AS group,\n s_group.name AS s_group,\n act_val.name + '(Data collection: ' + act_val.is_data_collected + ')' AS act_detail\nWITH\n version_dates,\n compare_type,\n act_detail,\n apoc.map.fromPairs(collect([study, flowchart_grp])) AS flowchart_grp,\n apoc.map.fromPairs(collect([study, group])) AS group,\n apoc.map.fromPairs(collect([study, s_group])) AS s_group\nWITH\n compare_type,\n version_dates['Base'] AS base_version_date,\n flowchart_grp['Base'] AS `SoA Group (Base)`,\n group['Base'] AS `Activity Group (Base)`,\n s_group['Base'] AS `Activity Subgroup (Base)`,\n version_dates['Compare'] AS compare_version_date,\n flowchart_grp['Compare'] AS `SoA Group (Compare)`,\n group['Compare'] AS `Activity Group (Compare)`,\n s_group['Compare'] AS `Activity Subgroup (Compare)`,\n act_detail AS `Activity Detail`\nWHERE `Activity Detail` IS NOT NULL\nWITH\n compare_type,\n base_version_date,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n compare_version_date,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`,\n CASE\n WHEN\n compare_type = 'Within Study'\n THEN\n CASE \n WHEN\n base_version_date <= compare_version_date AND\n `SoA Group (Base)` IS NOT NULL AND\n `SoA Group (Compare)` IS NULL\n THEN 'Activity deleted'\n ELSE\n CASE \n WHEN\n base_version_date <= compare_version_date AND\n `SoA Group (Base)` IS NULL AND\n `SoA Group (Compare)` IS NOT NULL\n THEN 'Activity added'\n ELSE\n CASE\n WHEN\n base_version_date >= compare_version_date AND\n `SoA Group (Base)` IS NOT NULL AND\n `SoA Group (Compare)` IS NULL\n THEN 'Activity added'\n ELSE\n CASE\n WHEN\n base_version_date >= compare_version_date AND\n `SoA Group (Base)` IS NULL AND\n `SoA Group (Compare)` IS NOT NULL\n THEN 'Activity deleted'\n ELSE\n CASE\n WHEN\n (NOT `SoA Group (Base)` = `SoA Group (Compare)`) OR\n (NOT `Activity Group (Base)` =\n `Activity Group (Compare)`) OR\n (NOT `Activity Subgroup (Base)` =\n `Activity Subgroup (Compare)`)\n THEN 'Activity moved'\n ELSE 'No change'\n END\n END\n END\n END\n END\n ELSE\n CASE\n WHEN\n `SoA Group (Base)` IS NOT NULL AND `SoA Group (Compare)` IS NULL\n THEN 'Activity deleted'\n ELSE\n CASE\n WHEN\n `SoA Group (Base)` IS NULL AND `SoA Group (Compare)` IS NOT NULL\n THEN 'Activity added'\n ELSE\n CASE\n WHEN\n (NOT `SoA Group (Base)` = `SoA Group (Compare)`) OR\n (NOT `Activity Group (Base)` = `Activity Group (Compare)`) OR\n (NOT `Activity Subgroup (Base)` =\n `Activity Subgroup (Compare)`)\n THEN 'Activity moved'\n ELSE 'No change'\n END\n END\n END\nEND AS `Change Type`\nWITH\n `Change Type`,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`\nRETURN\n `Change Type`,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`", + "query": "//Activites compare\nMATCH (sr:StudyRoot)-[r0:HAS_VERSION]->(s:StudyValue)\nWHERE id(s) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nWITH sr, s, r0.start_date as start_date\nWITH\n CASE\n WHEN count(distinct sr) > 1 THEN 'Across Study'\n ELSE 'Within Study'\n END AS compare_type,\n apoc.map.fromPairs(\n collect(\n CASE\n WHEN id(s) = toInteger($neodash_studya) THEN ['Base', start_date]\n ELSE ['Compare', start_date]\n END\n )\n ) AS version_dates\nWITH compare_type,version_dates\nMATCH (:StudyRoot)-[r0:HAS_VERSION]->(s:StudyValue)\nWHERE id(s) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nWITH\ncompare_type,\nversion_dates,\n r0,\n s,\n id(s) AS sid,\n CASE\n WHEN id(s) = toInteger($neodash_studya) THEN 'Base'\n ELSE 'Compare'\n END AS study\nMATCH\n (s)-[r3:HAS_STUDY_ACTIVITY]->\n (act)-[l1:HAS_SELECTED_ACTIVITY]->\n (act_val:ActivityValue)\n// Match the specific groups for THIS activity\nOPTIONAL MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(fl_group)\nOPTIONAL MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(s_act_grp)\nOPTIONAL MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(s_act_s_grp)\nOPTIONAL MATCH\n (s)-->\n (act)<-[:AFTER]-\n (action:StudyAction)-[:BEFORE]->\n (act_prev)<--\n (sx:StudyValue),\n (act_prev)-->(prev_act_val:ActivityValue)\nWHERE id(sx) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nOPTIONAL MATCH\n (s)-[:HAS_STUDY_ACTIVITY_SCHEDULE]->(schedule),\n (schedule)<-[:STUDY_ACTIVITY_HAS_SCHEDULE]-(act:StudyActivity),\n (act)-[:HAS_SELECTED_ACTIVITY]->(act_val:ActivityValue),\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_INSTANCE]->\n (ai_s:StudyActivityInstance)-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (ai:ActivityInstanceValue),\n (ai)-[r1]->(g)<-[r2]-(act_val)\nWITH version_dates, compare_type, study, act, act_val,prev_act_val,fl_group,s_act_grp,s_act_s_grp\nOPTIONAL MATCH\n (fl_group)-[:HAS_FLOWCHART_GROUP]->\n (ct_root:CTTermContext)-[:HAS_SELECTED_TERM]->\n (ct_name_root:CTTermRoot)-[:HAS_NAME_ROOT]->(name_root:CTTermNameRoot)-[:LATEST]->\n (flowchart_grp:CTTermNameValue)\nOPTIONAL MATCH\n (s_act_grp)-[:HAS_SELECTED_ACTIVITY_GROUP]->\n (group:ActivityGroupValue)\nOPTIONAL MATCH\n (s_act_s_grp)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (s_group:ActivitySubGroupValue)\nWITH DISTINCT\n version_dates,\n compare_type,\n study,\n flowchart_grp.name AS flowchart_grp,\n group.name AS group,\n s_group.name AS s_group,\n act_val.name + '(Data collection: ' + act_val.is_data_collected + ')' AS act_detail,\n prev_act_val.name as prev_activity\n// Create a unique key for each activity + group + subgroup combination\nWITH\n version_dates,\n compare_type,\n act_detail,\n prev_activity,\n coalesce(flowchart_grp, 'NULL') + '|' + coalesce(group, 'NULL') + '|' + coalesce(s_group, 'NULL') AS group_key,\n study,\n flowchart_grp,\n group,\n s_group\n// Now aggregate by activity detail AND group combination\nWITH\n version_dates,\n compare_type,\n act_detail,\n prev_activity,\n group_key,\n apoc.map.fromPairs(collect(DISTINCT [study, flowchart_grp])) AS flowchart_grp,\n apoc.map.fromPairs(collect(DISTINCT [study, group])) AS group,\n apoc.map.fromPairs(collect(DISTINCT [study, s_group])) AS s_group\nWITH\n compare_type,\n version_dates['Base'] AS base_version_date,\n flowchart_grp['Base'] AS `SoA Group (Base)`,\n group['Base'] AS `Activity Group (Base)`,\n s_group['Base'] AS `Activity Subgroup (Base)`,\n version_dates['Compare'] AS compare_version_date,\n flowchart_grp['Compare'] AS `SoA Group (Compare)`,\n group['Compare'] AS `Activity Group (Compare)`,\n s_group['Compare'] AS `Activity Subgroup (Compare)`,\n act_detail AS `Activity Detail`,\n prev_activity AS `Previous Activity`\nWHERE `Activity Detail` IS NOT NULL\nWITH\n compare_type,\n base_version_date,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n compare_version_date,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`,\n `Previous Activity`,\n CASE\n WHEN\n compare_type = 'Within Study'\n THEN\n CASE\n WHEN\n base_version_date <= compare_version_date AND\n `SoA Group (Base)` IS NOT NULL AND\n `SoA Group (Compare)` IS NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity deleted'\n END\n ELSE\n CASE\n WHEN\n base_version_date <= compare_version_date AND\n `SoA Group (Base)` IS NULL AND\n `SoA Group (Compare)` IS NOT NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity added'\n END\n ELSE\n CASE\n WHEN\n base_version_date >= compare_version_date AND\n `SoA Group (Base)` IS NOT NULL AND\n `SoA Group (Compare)` IS NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity added'\n END\n ELSE\n CASE\n WHEN\n base_version_date >= compare_version_date AND\n `SoA Group (Base)` IS NULL AND\n `SoA Group (Compare)` IS NOT NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity deleted'\n END\n ELSE\n CASE\n WHEN\n (coalesce(`SoA Group (Base)`, '') <> coalesce(`SoA Group (Compare)`, '')) OR\n (coalesce(`Activity Group (Base)`, '') <> coalesce(`Activity Group (Compare)`, '')) OR\n (coalesce(`Activity Subgroup (Base)`, '') <> coalesce(`Activity Subgroup (Compare)`, ''))\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity moved'\n END\n ELSE\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'No change'\n END\n END\n END\n END\n END\n END\n ELSE\n CASE\n WHEN\n `SoA Group (Base)` IS NOT NULL AND `SoA Group (Compare)` IS NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity deleted'\n END\n ELSE\n CASE\n WHEN\n `SoA Group (Base)` IS NULL AND `SoA Group (Compare)` IS NOT NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity added'\n END\n ELSE\n CASE\n WHEN\n (coalesce(`SoA Group (Base)`, '') <> coalesce(`SoA Group (Compare)`, '')) OR\n (coalesce(`Activity Group (Base)`, '') <> coalesce(`Activity Group (Compare)`, '')) OR\n (coalesce(`Activity Subgroup (Base)`, '') <> coalesce(`Activity Subgroup (Compare)`, ''))\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity moved'\n END\n ELSE\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'No change'\n END\n END\n END\n END\nEND AS `Change Type`\nWITH\n `Change Type`,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`,\n `Previous Activity`\nWHERE\n $neodash_differences_only = 'No' OR `Change Type` <> 'No change'\nRETURN\n `Change Type`,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`,\n `Previous Activity`", "width": 24, "height": 6, "x": 0, @@ -643,6 +659,13 @@ "value": "No change", "customization": "row color", "customizationValue": "#5FE8866E" + }, + { + "field": "Change Type", + "condition": "=", + "value": "Replaced", + "customization": "row color", + "customizationValue": "#CDA5E84D" } ], "compact": true, @@ -671,7 +694,7 @@ { "id": "89e64ef6-c2b6-431d-824d-d74750a056ec", "title": "Change Type", - "query": "with [\"Added\", \"No Change\", \"Moved\", \"Deleted\"] as list\nunwind(list) as Status\nreturn Status\n\n\n", + "query": "with [\"Added\", \"No Change\", \"Moved\", \"Deleted\", \"Replaced\"] as list\nunwind(list) as Status\nreturn Status\n\n\n", "width": 6, "height": 3, "x": 0, @@ -708,6 +731,76 @@ "value": "Deleted", "customization": "row color", "customizationValue": "#FF11116E" + }, + { + "field": "Status", + "condition": "=", + "value": "Replaced", + "customization": "row color", + "customizationValue": "#CDA5E84D" + } + ] + } + }, + { + "id": "0c998b38-c055-4aea-b921-c2f2702049a9", + "title": "SoA L1 - Protocol SoA view - grey is showing not displayed", + "query": "//Activites compare - FIXED\nMATCH (sr:StudyRoot)-[r0:HAS_VERSION]->(s:StudyValue)\nWHERE id(s) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nWITH sr, s, r0.start_date as start_date\nWITH\n CASE\n WHEN count(distinct sr) > 1 THEN 'Across Study'\n ELSE 'Within Study'\n END AS compare_type,\n apoc.map.fromPairs(\n collect(\n CASE\n WHEN id(s) = toInteger($neodash_studya) THEN ['Base', start_date]\n ELSE ['Compare', start_date]\n END\n )\n ) AS version_dates\nWITH compare_type,version_dates\nMATCH (:StudyRoot)-[r0:HAS_VERSION]->(s:StudyValue)\nWHERE id(s) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nWITH\ncompare_type,\nversion_dates,\n r0,\n s,\n id(s) AS sid,\n CASE\n WHEN id(s) = toInteger($neodash_studya) THEN 'Base'\n ELSE 'Compare'\n END AS study\nMATCH\n (s)-[r3:HAS_STUDY_ACTIVITY]->\n (act)-[l1:HAS_SELECTED_ACTIVITY]->\n (act_val:ActivityValue)\n// Match the specific groups for THIS activity\n MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(fl_group)<--(s)\n MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(s_act_grp)<--(s)\n MATCH\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(s_act_s_grp)<--(s)\nOPTIONAL MATCH\n (s)-->\n (act)<-[:AFTER]-\n (action:StudyAction)-[:BEFORE]->\n (act_prev)<--\n (sx:StudyValue),\n (act_prev)-->(prev_act_val:ActivityValue)\nWHERE id(sx) IN [toInteger($neodash_studya), toInteger($neodash_studyb)]\nOPTIONAL MATCH\n (s)-[:HAS_STUDY_ACTIVITY_SCHEDULE]->(schedule),\n (schedule)<-[:STUDY_ACTIVITY_HAS_SCHEDULE]-(act:StudyActivity),\n (act)-[:HAS_SELECTED_ACTIVITY]->(act_val:ActivityValue),\n (act)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_INSTANCE]->\n (ai_s:StudyActivityInstance)-[:HAS_SELECTED_ACTIVITY_INSTANCE]-\n (ai:ActivityInstanceValue),\n (ai)-[r1]->(g)<-[r2]-(act_val)\nWITH version_dates, compare_type, study, act, act_val,prev_act_val,fl_group,s_act_grp,s_act_s_grp\nOPTIONAL MATCH\n (fl_group)-[:HAS_FLOWCHART_GROUP]->\n (ct_root:CTTermContext)-[:HAS_SELECTED_TERM]->\n (ct_name_root:CTTermRoot)-[:HAS_NAME_ROOT]->(name_root:CTTermNameRoot)-[:LATEST]->\n (flowchart_grp:CTTermNameValue)\nOPTIONAL MATCH\n (s_act_grp)-[:HAS_SELECTED_ACTIVITY_GROUP]->\n (group:ActivityGroupValue)\nOPTIONAL MATCH\n (s_act_s_grp)-[:HAS_SELECTED_ACTIVITY_SUBGROUP]->\n (s_group:ActivitySubGroupValue)\nWITH DISTINCT\n version_dates,\n compare_type,\n study,\n flowchart_grp.name AS flowchart_grp,\n group.name AS group,\n s_group.name AS s_group,\n act_val.name + '(Data collection: ' + act_val.is_data_collected + ')' AS act_detail,\n prev_act_val.name as prev_activity,\n fl_group.show_soa_group_in_protocol_flowchart AS fl_group_display_raw,\n s_act_grp.show_activity_group_in_protocol_flowchart AS s_act_grp_display_raw,\n s_act_s_grp.show_activity_subgroup_in_protocol_flowchart AS s_act_s_grp_display_raw\n// Create a unique key for each activity + group + subgroup combination\nWITH\n version_dates,\n compare_type,\n act_detail,\n prev_activity,\n coalesce(flowchart_grp, 'NULL') + '|' + coalesce(group, 'NULL') + '|' + coalesce(s_group, 'NULL') AS group_key,\n study,\n flowchart_grp,\n group,\n s_group,\n // Convert boolean to display string HERE, per row\n CASE WHEN coalesce(fl_group_display_raw, false) THEN 'display' ELSE 'not-display' END AS fl_group_display,\n CASE WHEN coalesce(s_act_grp_display_raw, false) THEN 'display' ELSE 'not-display' END AS s_act_grp_display,\n CASE WHEN coalesce(s_act_s_grp_display_raw, false) THEN 'display' ELSE 'not-display' END AS s_act_s_grp_display\n// Now aggregate by activity detail AND group combination\nWITH\n version_dates,\n compare_type,\n act_detail,\n prev_activity,\n group_key,\n apoc.map.fromPairs(collect(DISTINCT [study, flowchart_grp])) AS flowchart_grp,\n apoc.map.fromPairs(collect(DISTINCT [study, group])) AS group,\n apoc.map.fromPairs(collect(DISTINCT [study, s_group])) AS s_group,\n // Use MAX or MIN to pick one value (they should all be the same per study)\n apoc.map.fromPairs(collect(DISTINCT [study, fl_group_display])) AS fl_group_display,\n apoc.map.fromPairs(collect(DISTINCT [study, s_act_grp_display])) AS s_act_grp_display,\n apoc.map.fromPairs(collect(DISTINCT [study, s_act_s_grp_display])) AS s_act_s_grp_display\nWITH\n compare_type,\n version_dates['Base'] AS base_version_date,\n flowchart_grp['Base'] AS `SoA Group (Base)`,\n group['Base'] AS `Activity Group (Base)`,\n s_group['Base'] AS `Activity Subgroup (Base)`,\n version_dates['Compare'] AS compare_version_date,\n flowchart_grp['Compare'] AS `SoA Group (Compare)`,\n group['Compare'] AS `Activity Group (Compare)`,\n s_group['Compare'] AS `Activity Subgroup (Compare)`,\n act_detail AS `Activity Detail`,\n prev_activity AS `Previous Activity`,\n fl_group_display['Base'] AS `SoA Display (Base)`,\n fl_group_display['Compare'] AS `SoA Display (Compare)`,\n s_act_grp_display['Base'] AS `Activity Group Display (Base)`,\n s_act_grp_display['Compare'] AS `Activity Group Display (Compare)`,\n s_act_s_grp_display['Base'] AS `Activity Subgroup Display (Base)`,\n s_act_s_grp_display['Compare'] AS `Activity Subgroup Display (Compare)`\nWHERE `Activity Detail` IS NOT NULL\nWITH\n compare_type,\n base_version_date,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n compare_version_date,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`,\n `Previous Activity`,\n `SoA Display (Base)`,\n `SoA Display (Compare)`,\n `Activity Group Display (Base)`,\n `Activity Group Display (Compare)`,\n `Activity Subgroup Display (Base)`,\n `Activity Subgroup Display (Compare)`,\n // Calculate display change indicators\n CASE\n WHEN coalesce(`SoA Display (Base)`, 'not-display') <> coalesce(`SoA Display (Compare)`, 'not-display')\n THEN 'Changed'\n ELSE 'No change'\n END AS `SoA Display Change`,\n CASE\n WHEN coalesce(`Activity Group Display (Base)`, 'not-display') <> coalesce(`Activity Group Display (Compare)`, 'not-display')\n THEN 'Changed'\n ELSE 'No change'\n END AS `Activity Group Display Change`,\n CASE\n WHEN coalesce(`Activity Subgroup Display (Base)`, 'not-display') <> coalesce(`Activity Subgroup Display (Compare)`, 'not-display')\n THEN 'Changed'\n ELSE 'No change'\n END AS `Activity Subgroup Display Change`,\n CASE\n WHEN\n compare_type = 'Within Study'\n THEN\n CASE\n WHEN\n base_version_date <= compare_version_date AND\n `SoA Group (Base)` IS NOT NULL AND\n `SoA Group (Compare)` IS NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity deleted'\n END\n ELSE\n CASE\n WHEN\n base_version_date <= compare_version_date AND\n `SoA Group (Base)` IS NULL AND\n `SoA Group (Compare)` IS NOT NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity added'\n END\n ELSE\n CASE\n WHEN\n base_version_date >= compare_version_date AND\n `SoA Group (Base)` IS NOT NULL AND\n `SoA Group (Compare)` IS NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity added'\n END\n ELSE\n CASE\n WHEN\n base_version_date >= compare_version_date AND\n `SoA Group (Base)` IS NULL AND\n `SoA Group (Compare)` IS NOT NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity deleted'\n END\n ELSE\n CASE\n WHEN\n (coalesce(`SoA Group (Base)`, '') <> coalesce(`SoA Group (Compare)`, '')) OR\n (coalesce(`Activity Group (Base)`, '') <> coalesce(`Activity Group (Compare)`, '')) OR\n (coalesce(`Activity Subgroup (Base)`, '') <> coalesce(`Activity Subgroup (Compare)`, '')) OR\n (coalesce(`SoA Display (Base)`, 'not-display') <> coalesce(`SoA Display (Compare)`, 'not-display')) OR\n (coalesce(`Activity Group Display (Base)`, 'not-display') <> coalesce(`Activity Group Display (Compare)`, 'not-display')) OR\n (coalesce(`Activity Subgroup Display (Base)`, 'not-display') <> coalesce(`Activity Subgroup Display (Compare)`, 'not-display'))\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity moved'\n END\n ELSE\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'No change'\n END\n END\n END\n END\n END\n END\n ELSE\n CASE\n WHEN\n `SoA Group (Base)` IS NOT NULL AND `SoA Group (Compare)` IS NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity deleted'\n END\n ELSE\n CASE\n WHEN\n `SoA Group (Base)` IS NULL AND `SoA Group (Compare)` IS NOT NULL\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity added'\n END\n ELSE\n CASE\n WHEN\n (coalesce(`SoA Group (Base)`, '') <> coalesce(`SoA Group (Compare)`, '')) OR\n (coalesce(`Activity Group (Base)`, '') <> coalesce(`Activity Group (Compare)`, '')) OR\n (coalesce(`Activity Subgroup (Base)`, '') <> coalesce(`Activity Subgroup (Compare)`, '')) OR\n (coalesce(`SoA Display (Base)`, 'not-display') <> coalesce(`SoA Display (Compare)`, 'not-display')) OR\n (coalesce(`Activity Group Display (Base)`, 'not-display') <> coalesce(`Activity Group Display (Compare)`, 'not-display')) OR\n (coalesce(`Activity Subgroup Display (Base)`, 'not-display') <> coalesce(`Activity Subgroup Display (Compare)`, 'not-display'))\n THEN\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'Activity moved'\n END\n ELSE\n CASE\n WHEN `Previous Activity` IS NOT NULL THEN 'Replaced'\n ELSE 'No change'\n END\n END\n END\n END\nEND AS `Change Type`\nWITH\n `Change Type`,\n `SoA Display Change`,\n `Activity Group Display Change`,\n `Activity Subgroup Display Change`,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`,\n `Previous Activity`,\n `SoA Display (Base)`,\n `SoA Display (Compare)`,\n `Activity Group Display (Base)`,\n `Activity Group Display (Compare)`,\n `Activity Subgroup Display (Base)`,\n `Activity Subgroup Display (Compare)`\nWHERE\n $neodash_differences_only = 'No' //OR `Change Type` <> 'No change'\nRETURN\n `Change Type`,\n `SoA Group (Base)`,\n `Activity Group (Base)`,\n `Activity Subgroup (Base)`,\n `SoA Group (Compare)`,\n `Activity Group (Compare)`,\n `Activity Subgroup (Compare)`,\n `Activity Detail`,\n `Previous Activity`,\n `SoA Display (Base)` as __soa_base_disp,\n `SoA Display (Compare)` as __soa_comp_disp,\n `Activity Group Display (Base)` as __grp_base_disp,\n `Activity Group Display (Compare)` as __grp_comp_disp ,\n `Activity Subgroup Display (Base)` as __sgrp_base_disp,\n `Activity Subgroup Display (Compare)` as __sgrp_comp_disp", + "width": 24, + "height": 6, + "x": 0, + "y": 9, + "type": "table", + "selection": {}, + "settings": { + "styleRules": [ + { + "field": "__soa_base_disp", + "condition": "=", + "value": "not-display", + "customization": "cell text color", + "customizationValue": "lightgrey", + "targetField": "SoA Group (Base)" + }, + { + "field": "__soa_comp_disp", + "condition": "=", + "value": "not-display", + "customization": "cell text color", + "customizationValue": "lightgrey", + "targetField": "SoA Group (Compare)" + }, + { + "field": "__grp_base_disp", + "condition": "=", + "value": "not-display", + "customization": "cell text color", + "customizationValue": "lightgrey", + "targetField": "Activity Group (Base)" + }, + { + "field": "__grp_comp_disp", + "condition": "=", + "value": "not-display", + "customization": "cell text color", + "customizationValue": "lightgrey", + "targetField": "Activity Group (Compare)" + }, + { + "field": "__sgrp_base_disp", + "condition": "=", + "value": "not-display", + "customization": "cell text color", + "customizationValue": "lightgrey", + "targetField": "Activity Subgroup (Base)" + }, + { + "field": "__sgrp_comp_disp", + "condition": "=", + "value": "not-display", + "customization": "cell text color", + "customizationValue": "lightgrey", + "targetField": "Activity Subgroup (Compare)" } ] } diff --git a/studybuilder-import/.env.e2e b/studybuilder-import/.env.e2e index 02bb59e7..ab5a5549 100644 --- a/studybuilder-import/.env.e2e +++ b/studybuilder-import/.env.e2e @@ -152,13 +152,14 @@ MDR_MIGRATION_ELEMENT_SUBTYPE="e2e_datafiles/sponsor_library/element/element_sub # MDR_MIGRATION_ODM_VENDOR_NAMESPACES="e2e_datafiles/libraries/concepts/crfs/odm_vendor_namespaces.csv" MDR_MIGRATION_ODM_VENDOR_ATTRIBUTES="e2e_datafiles/libraries/concepts/crfs/odm_vendor_attributes.csv" -MDR_MIGRATION_ODM_TEMPLATES="e2e_datafiles/libraries/concepts/crfs/odm_templates.csv" +MDR_MIGRATION_ODM_STUDY_EVENTS="e2e_datafiles/libraries/concepts/crfs/odm_study_events.csv" MDR_MIGRATION_ODM_FORMS="e2e_datafiles/libraries/concepts/crfs/odm_forms.csv" -MDR_MIGRATION_ODM_ITEMGROUPS="e2e_datafiles/libraries/concepts/crfs/odm_itemgroups.csv" +MDR_MIGRATION_ODM_ITEM_GROUPS="e2e_datafiles/libraries/concepts/crfs/odm_item_groups.csv" MDR_MIGRATION_ODM_ITEMS="e2e_datafiles/libraries/concepts/crfs/odm_items.csv" -MDR_MIGRATION_ODM_TEMPLATE_TO_FORM_RELATIONSHIP="e2e_datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv" -MDR_MIGRATION_ODM_FORM_TO_ITEMGROUP_RELATIONSHIP="e2e_datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv" -MDR_MIGRATION_ODM_ITEMGROUP_TO_ITEM_RELATIONSHIP="e2e_datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv" +MDR_MIGRATION_ODM_FORMS_TO_ODM_STUDY_EVENTS="e2e_datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv" +MDR_MIGRATION_ODM_ITEM_GROUPS_TO_ODM_FORMS="e2e_datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv" +MDR_MIGRATION_ODM_ITEMS_TO_ODM_ITEM_GROUPS="e2e_datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv" +MDR_MIGRATION_ODM_ITEMS_TO_ACTIVITY_INSTANCES="e2e_datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv" # # Unsorted for sponsor library @@ -223,6 +224,7 @@ MDR_MIGRATION_STUDY_TYPE="e2e_datafiles/mockup/migrated_ts_definitions_exp.csv" # MDR_MIGRATION_ACTIVITY_INSTANCE_CLASS_MODEL_RELS="e2e_datafiles/sponsor_library/sponsormodel/dataset_class_activity_instance_class_full.csv" MDR_MIGRATION_ACTIVITY_ITEM_CLASS_MODEL_RELS="e2e_datafiles/sponsor_library/sponsormodel/variable_class_activity_item_class_full.csv" +MDR_MIGRATION_ACTIVITY_ITEM_CLASS_VALID_CODELIST_RELS="e2e_datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv" MDR_MIGRATION_SPONSOR_MODEL_DIRECTORY="e2e_datafiles/sponsor_library/sponsormodel" MDR_MIGRATION_SPONSOR_MODEL_DATASETS="ReferenceTables.csv" MDR_MIGRATION_SPONSOR_MODEL_DATASET_VARIABLES="ReferenceColumns.csv" @@ -235,8 +237,8 @@ IMPORT_PROJECTS="e2e_datafiles/mockup/exported/projects.json" MDR_MIGRATION_STUDY_VERSIONS=True MDR_MIGRATION_VERSIONED_STUDY_PATH="e2e_datafiles/mockup/exported/versioned_study" # Study Milestone Codelists -MDR_MIGRATION_LOCK_STUDY_MILESTONE="e2e_datafiles/sponsor_library/lock_study_milestone.csv" -MDR_MIGRATION_UNLOCK_STUDY_MILESTONE="e2e_datafiles/sponsor_library/unlock_study_milestone.csv" +MDR_MIGRATION_REASON_FOR_LOCK="e2e_datafiles/sponsor_library/reason_for_lock.csv" +MDR_MIGRATION_REASON_FOR_UNLOCK="e2e_datafiles/sponsor_library/reason_for_unlock.csv" # Complexity Score MDR_COMPLEXITY_BURDENS="datafiles/complexity_score/burdens.csv" diff --git a/studybuilder-import/.env.import b/studybuilder-import/.env.import index c3452c9a..3920f51d 100644 --- a/studybuilder-import/.env.import +++ b/studybuilder-import/.env.import @@ -153,13 +153,14 @@ MDR_MIGRATION_ELEMENT_SUBTYPE="datafiles/sponsor_library/element/element_subtype # MDR_MIGRATION_ODM_VENDOR_NAMESPACES="datafiles/libraries/concepts/crfs/odm_vendor_namespaces.csv" MDR_MIGRATION_ODM_VENDOR_ATTRIBUTES="datafiles/libraries/concepts/crfs/odm_vendor_attributes.csv" -MDR_MIGRATION_ODM_TEMPLATES="datafiles/libraries/concepts/crfs/odm_templates.csv" +MDR_MIGRATION_ODM_STUDY_EVENTS="datafiles/libraries/concepts/crfs/odm_study_events.csv" MDR_MIGRATION_ODM_FORMS="datafiles/libraries/concepts/crfs/odm_forms.csv" -MDR_MIGRATION_ODM_ITEMGROUPS="datafiles/libraries/concepts/crfs/odm_itemgroups.csv" +MDR_MIGRATION_ODM_ITEM_GROUPS="datafiles/libraries/concepts/crfs/odm_item_groups.csv" MDR_MIGRATION_ODM_ITEMS="datafiles/libraries/concepts/crfs/odm_items.csv" -MDR_MIGRATION_ODM_TEMPLATE_TO_FORM_RELATIONSHIP="datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv" -MDR_MIGRATION_ODM_FORM_TO_ITEMGROUP_RELATIONSHIP="datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv" -MDR_MIGRATION_ODM_ITEMGROUP_TO_ITEM_RELATIONSHIP="datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv" +MDR_MIGRATION_ODM_FORMS_TO_ODM_STUDY_EVENTS="datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv" +MDR_MIGRATION_ODM_ITEM_GROUPS_TO_ODM_FORMS="datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv" +MDR_MIGRATION_ODM_ITEMS_TO_ODM_ITEM_GROUPS="datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv" +MDR_MIGRATION_ODM_ITEMS_TO_ACTIVITY_INSTANCES="datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv" # # Unsorted for sponsor library @@ -224,6 +225,7 @@ MDR_MIGRATION_STUDY_TYPE="datafiles/mockup/migrated_ts_definitions_exp.csv" # MDR_MIGRATION_ACTIVITY_INSTANCE_CLASS_MODEL_RELS="datafiles/sponsor_library/sponsormodel/dataset_class_activity_instance_class_full.csv" MDR_MIGRATION_ACTIVITY_ITEM_CLASS_MODEL_RELS="datafiles/sponsor_library/sponsormodel/variable_class_activity_item_class_full.csv" +MDR_MIGRATION_ACTIVITY_ITEM_CLASS_VALID_CODELIST_RELS="datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv" MDR_MIGRATION_SPONSOR_MODEL_DIRECTORY="datafiles/sponsor_library/sponsormodel" MDR_MIGRATION_SPONSOR_MODEL_DATASETS="ReferenceTables.csv" MDR_MIGRATION_SPONSOR_MODEL_DATASET_VARIABLES="ReferenceColumns.csv" @@ -236,9 +238,8 @@ IMPORT_PROJECTS="datafiles/mockup/exported/projects.json" MDR_MIGRATION_STUDY_VERSIONS=False MDR_MIGRATION_VERSIONED_STUDY_PATH="" # Study Milestone Codelists -MDR_MIGRATION_LOCK_STUDY_MILESTONE="datafiles/sponsor_library/lock_study_milestone.csv" -MDR_MIGRATION_UNLOCK_STUDY_MILESTONE="datafiles/sponsor_library/unlock_study_milestone.csv" - +MDR_MIGRATION_REASON_FOR_LOCK="datafiles/sponsor_library/reason_for_lock.csv" +MDR_MIGRATION_REASON_FOR_UNLOCK="datafiles/sponsor_library/reason_for_unlock.csv" # Complexity Score MDR_COMPLEXITY_BURDENS="datafiles/complexity_score/burdens.csv" diff --git a/studybuilder-import/datafiles/configuration/feature_flags.csv b/studybuilder-import/datafiles/configuration/feature_flags.csv index 922c4401..e4e6aff7 100644 --- a/studybuilder-import/datafiles/configuration/feature_flags.csv +++ b/studybuilder-import/datafiles/configuration/feature_flags.csv @@ -3,8 +3,12 @@ studies_view_listings_analysis_study_metadata_new,1,"This flag toggles on/off th new_activity_instance_wizard_stepper,1,"This flag toggles on/off the new wizard stepper to create/edit Activity Instances (Library/Concepts/Activities/Activity Instances)" activity_instance_wizard_stepper_categoric_findings,0,"This flag toggles on/off the Categoric Findings use case in the new wizard stepper for activity instances" activity_instance_wizard_stepper_textual_findings,0,"This flag toggles on/off the Textual Findings use case in the new wizard stepper for activity instances" +activity_instance_wizard_stepper_edit_mode,0,"This flag toggles on/off the edit mode of the new wizard stepper for activity instances" compounds_library,0,"This flag toggles on/off the whole subpage found at Library/Concepts/Compounds" compounds_studies,0,"This flag toggles on/off the whole subpage found at Studies/Define Study/Study Interventions" complexity_score_calculation,0,"Enables complexity score calculation on the Detailed SoA page" study_data_suppliers,0,"This flag toggles on/off the Study Data Suppliers page found at Studies/Manage/Study Data Suppliers" study_data_suppliers_create_from_study,0,"This flag toggles on/off the ability to create a user-defined data supplier from study level (Add a user defined data supplier button)" +prodex_extension,0,"Show/hide admin menu for managing extracts and loads of ProdEx data into OSB" +meddra_dictionary,0,"This flag toggles on/off the MedDRA dictionary page found at Library/Dictionaries/MedDRA" +loinc_dictionary,0,"This flag toggles on/off the LOINC dictionary page found at Library/Dictionaries/LOINC" \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms.csv index 0dcd57b9..9efcf224 100644 --- a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms.csv +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms.csv @@ -1,24 +1,24 @@ -library,oid,name,prompt,repeating,language,description,instruction,aliases -Sponsor,F.DM,Informed Consent and Demography,,false,ENG,Informed Consent and Demography form,"Please complete this Informed Consent and Demography form at the very beginning of the study - -General item design notes: -Integration: A: Argus, Ax: Forms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM - -General item design notes: Integration: A: Argus, Ax: - -rms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM - -Oracle item des -N notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/DemogOracle item design notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/Demog", -Sponsor,F.VS,Vital Signs,,false,ENG,Vital signs form,Please complete this Vital Sign form before starting the treatment, -Sponsor,F.MH,Medical History/Concomitant Illness,,false,ENG,Medical History/Concomitant Illness (Without pre-printed diagnosis),"This CRF is to be used for studies not collecting specific medical histories (e.g. Cardiovascular History, Diabetes History, Gallbladder Disease History).", -Sponsor,F.IM,Administration of ,,false,ENG,Administration of ,"In case of multiple investigational medicinal products, there should be separate CRFs for each, unless the study in blinded. -In case the study is blinded, the investigational medical product should be named with combination of the drugs with a '/' between the names. -The items: ‘Dose form’ and ‘Route’ do not need to be visible for investigator if only one response is used, but values must be available in the data loaded. -If stop date is not collected, copy start date (ECSTDTC) to stop date (ECENDTC). This could be cases for administrations considered given at a point in time (e.g., oral tablet, pre-filled syringe injection). -Either Prescribed dose or Actual dose has to be collected together with the Start date. -This form is a maximum content form and no additional questions and/or response options can be added unless evaluated E2E by IG and approved by the PST.", -Sponsor,F.AE,Adverse Event,,false,ENG,Adverse Event,"One AE should be reported per form., -During conduct of the study, please transcribe data to EDC as soon as possible. -The AE diagnosis, causality, seriousness and severity should be evaluated by the investigator or sub-investigator with physician background.", -Sponsor,F.EG,ECG,,false,ENG,ECG form,Please complete this ECG form before starting the treatment, \ No newline at end of file +oid,name,repeating,sdtm_version,translated_texts,aliases +F.DM,Informed Consent and Demography,No,,"Description::en::Informed Consent and Demography form||osb:CompletionInstructions::en::Please complete this Informed Consent and Demography form at the very beginning of the study + +General item design notes: +Integration: A: Argus, Ax: Forms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM + +General item design notes: Integration: A: Argus, Ax: + +rms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM + +Oracle item des +N notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/DemogOracle item design notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/Demog", +F.VS,Vital Signs,No,,"Description::en::Vital signs form||osb:CompletionInstructions::en::Please complete this Vital Sign form before starting the treatment", +F.MH,Medical History/Concomitant Illness,No,,"Description::en::Medical History/Concomitant Illness (Without pre-printed diagnosis)||osb:CompletionInstructions::en::This CRF is to be used for studies not collecting specific medical histories (e.g. Cardiovascular History, Diabetes History, Gallbladder Disease History).", +F.IM,Administration of ,No,,"Description::en::Administration of ||osb:CompletionInstructions::en::In case of multiple investigational medicinal products, there should be separate CRFs for each, unless the study in blinded. +In case the study is blinded, the investigational medical product should be named with combination of the drugs with a '/' between the names. +The items: ‘Dose form’ and ‘Route’ do not need to be visible for investigator if only one response is used, but values must be available in the data loaded. +If stop date is not collected, copy start date (ECSTDTC) to stop date (ECENDTC). This could be cases for administrations considered given at a point in time (e.g., oral tablet, pre-filled syringe injection). +Either Prescribed dose or Actual dose has to be collected together with the Start date. +This form is a maximum content form and no additional questions and/or response options can be added unless evaluated E2E by IG and approved by the PST.", +F.AE,Adverse Event,No,,"Description::en::Adverse Event||osb:CompletionInstructions::en::One AE should be reported per form., +During conduct of the study, please transcribe data to EDC as soon as possible. +The AE diagnosis, causality, seriousness and severity should be evaluated by the investigator or sub-investigator with physician background.", +F.EG,ECG,No,,"Description::en::ECG form||osb:CompletionInstructions::en::Please complete this ECG form before starting the treatment", \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv deleted file mode 100644 index ddf2d5a9..00000000 --- a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv +++ /dev/null @@ -1,7 +0,0 @@ -uid_form,oid_form,uid_itemgroup,oid_itemgroup,order_number,mandatory,collection_exception_condition_oid -F.DM,F.DM,G.DM.IC,G.DM.IC,1,TRUE, -F.DM,F.DM,G.DM.DM,G.DM.DM,2,TRUE, -F.MH,F.MH,G.MH.NS,G.MH.NS,1,TRUE, -F.MH,F.MH,G.MH.CM,G.MH.CM,2,TRUE, -F.VS,F.VS,G.VS.VS,G.VS.VS,1,TRUE, -F.VS,F.VS,G.VS.BPP,G.VS.BPP,2,TRUE, \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv new file mode 100644 index 00000000..bd290945 --- /dev/null +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv @@ -0,0 +1,7 @@ +form_oid,study_event_oid,order_number,mandatory,locked,collection_exception_condition_oid +F.DM,T.ODM-1-3-2-V1,1,No,No, +F.VS,T.ODM-1-3-2-V1,1,No,No, +F.MH,T.ODM-1-3-2-V1,1,No,No, +F.IM,T.ODM-1-3-2-V1,1,No,No, +F.AE,T.ODM-1-3-2-V1,1,No,No, +F.EG,T.ODM-1-3-2-V1,1,No,No, \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_item_groups.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_item_groups.csv new file mode 100644 index 00000000..45d9ae68 --- /dev/null +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_item_groups.csv @@ -0,0 +1,8 @@ +oid,name,repeating,is_reference_data,sas_dataset_name,origin,purpose,comment,sdtm_domains,translated_texts,aliases +G.DM.IC,Informed Consent,No,No,DEMOG,Collected Value,Tabulation,,DM||DS,"Description::en::Informed Consent item group||osb:CompletionInstructions::en::Please complete the Informed Consent item group before any other information", +G.DM.DM,General Demography,No,No,DEMOG,Collected Value,Tabulation,,DM,"Description::en::General Demographic item group||osb:CompletionInstructions::en::Please complete this General Demographic item group at the very beginning of the study", +G.MH.NS,Any conditions / illnesses,No,No,MEDHIS,Collected Value,Tabulation,,MH,"Description::en::Any conditions / illnesses ?||osb:CompletionInstructions::en::Please state if there was any conditions / Illnesses", +G.MH.CM,Medical History / Concomitant Illness,Yes,No,MEDHIS,Collected Value,Tabulation,,MH,"Description::en::Medical History item group||osb:CompletionInstructions::en::Please complete this Medical History item group before starting the treatment", +G.VS.VS,Vital Signs,Yes,No,VITALSIGNS,Collected Value,Tabulation,,VS,"Description::en::Vital signs||osb:CompletionInstructions::en::Please complete the Vital Signs item group at each expected time point", +G.VS.BPP,Blood pressure and pulse,No,No,VITALSIGNSBPP,Collected Value,Tabulation,,VS,"Description::en::Blood pressure and pulse||osb:CompletionInstructions::en::Please complete the Blood pressure and Pulse item group at each expected time point", +G.EG.ECGSTATUS,Is teh ECG,No,No,ECGSTATUS,Collected Value,Tabulation,,EG,"Description::en::ECG Test Results||osb:CompletionInstructions::en::Please complete the ECG Test Results item group at each expected time point", \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv new file mode 100644 index 00000000..60b3cd42 --- /dev/null +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv @@ -0,0 +1,7 @@ +item_group_oid,form_oid,order_number,mandatory,collection_exception_condition_oid +G.DM.IC,F.DM,1,Yes, +G.DM.DM,F.DM,2,Yes, +G.MH.NS,F.MH,1,Yes, +G.MH.CM,F.MH,2,Yes, +G.VS.VS,F.VS,1,Yes, +G.VS.BPP,F.VS,2,Yes, \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_itemgroups.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_itemgroups.csv deleted file mode 100644 index 20e8afa5..00000000 --- a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_itemgroups.csv +++ /dev/null @@ -1,8 +0,0 @@ -library,oid,name,prompt,repeating,isreferencedata,sasdatasetname,domain,origin,purpose,comment,language,description,instruction,aliases -Sponsor,G.DM.IC,Informed Consent,,false,false,DEMOG,"DM|DS",Collected Value,Tabulation,,ENG,Informed Consent item group,Please complete the Informed Consent item group before any other information, -Sponsor,G.DM.DM,General Demography ,,false,false,DEMOG,DM,Collected Value,Tabulation,,ENG,General Demographic item group,Please complete this General Demographic item group at the very beginning of the study, -Sponsor,G.MH.NS,Any conditions / illnesses,,false,false,MEDHIS,MH,Collected Value,Tabulation,,ENG,Any conditions / illnesses ?,Please state if there was any conditions / Illnesses, -Sponsor,G.MH.CM,Medical History / Concomitant Illness,,true,false,MEDHIS,MH,Collected Value,Tabulation,,ENG,Medical History item group,Please complete this Medical History item group before starting the treatment, -Sponsor,G.VS.VS,Vital Signs,,true,false,VITALSIGNS,VS,Collected Value,Tabulation,,ENG,Vital signs,Please complete the Vital Signs item group at each expected time point, -Sponsor,G.VS.BPP,Blood pressure and pulse,,false,false,VITALSIGNSBPP,VS,Collected Value,Tabulation,,ENG,Blood pressure and pulse,Please complete the Blood pressure and Pulse item group at each expected time point, -Sponsor,G.EG.ECGSTATUS,Is teh ECG,,false,false,ECGSTATUS,EG,Collected Value,Tabulation,,ENG,ECG Test Results,Please complete the ECG Test Results item group at each expected time point, \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv deleted file mode 100644 index 1cd660e7..00000000 --- a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv +++ /dev/null @@ -1,29 +0,0 @@ -uid_itemgroup,oid_itemgroup,uid_item,oid_item,order_number,mandatory,key_sequence,method_oid,imputation_method_oid,role,role_codelist_oid,collection_exception_condition_oid -G.DM.IC,G.DM.IC,I.STUDYID,I.STUDYID,1,TRUE,,,,,, -G.DM.IC,G.DM.IC,I.RFICDAT,I.RFICDAT,2,TRUE,,,,,, -G.DM.IC,G.DM.IC,I.RFICTIM,I.RFICTIM,3,TRUE,,,,,, -G.DM.IC,G.DM.IC,I.RFICDATLAR1,I.RFICDATLAR1,4,FALSE,,,,,, -G.DM.IC,G.DM.IC,I.RFICTIMLAR1,I.RFICTIMLAR1,5,FALSE,,,,,, -G.DM.IC,G.DM.IC,I.RFICDATLAR2,I.RFICDATLAR2,6,FALSE,,,,,, -G.DM.IC,G.DM.IC,I.RFICTIMLAR2,I.RFICTIMLAR2,7,FALSE,,,,,, -G.DM.DM,G.DM.DM,I.BRTHDTCARGUS,I.BRTHDTCARGUS,1,TRUE,,,,,, -G.DM.DM,G.DM.DM,I.BRTHDTC,I.BRTHDTC,2,TRUE,,,,,, -G.DM.DM,G.DM.DM,I.AGE,I.AGE,3,TRUE,,,,,, -G.DM.DM,G.DM.DM,I.SEX,I.SEX,4,FALSE,,,,,, -G.DM.DM,G.DM.DM,I.SEXDEA,I.SEXDEA,5,TRUE,,,,,, -G.DM.DM,G.DM.DM,I.ETHNIC,I.ETHNIC,6,TRUE,,,,,, -G.DM.DM,G.DM.DM,I.RACE,I.RACE,7,TRUE,,,,,, -G.DM.DM,G.DM.DM,I.RACEOTH,I.RACEOTH,8,FALSE,,,,,, -G.DM.DM,G.DM.DM,I.SUBJID,I.SUBJID,9,TRUE,,,,,, -G.DM.DM,G.DM.DM,I.PREVSUBJ,I.PREVSUBJ,10,FALSE,,,,,, -G.MH.NS,G.MH.NS,I.STUDYID,I.STUDYID,1,TRUE,,,,,, -G.MH.NS,G.MH.NS,I.SUBJID,I.SUBJID,2,TRUE,,,,,, -G.VS.VS,G.VS.VS,I.STUDYID,I.STUDYID,1,TRUE,,,,,, -G.VS.VS,G.VS.VS,I.SUBJID,I.SUBJID,2,TRUE,,,,,, -G.VS.VS,G.VS.VS,I.VSDAT,I.VSDAT,3,TRUE,,,,,, -G.VS.BPP,G.VS.BPP,I.SYSBP,I.SYSBP,1,TRUE,,,,,, -G.VS.BPP,G.VS.BPP,I.DIABP,I.DIABP,2,TRUE,,,,,, -G.VS.BPP,G.VS.BPP,I.POSITION,I.POSITION,3,TRUE,,,,,, -G.VS.BPP,G.VS.BPP,I.LATERALITY,I.LATERALITY,4,TRUE,,,,,, -G.VS.BPP,G.VS.BPP,I.LOC,I.LOC,5,TRUE,,,,,, -G.VS.BPP,G.VS.BPP,I.PULSE,I.PULSE,6,TRUE,,,,,, \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items.csv index 82a1dec3..867c14e5 100644 --- a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items.csv +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items.csv @@ -1,25 +1,25 @@ -library,oid,name,prompt,datatype,length,significantdigits,codelist,term,unit,sasfieldname,sdsvarname,origin,comment,language,description,instruction,aliases -Sponsor,I.STUDYID,Study ID,StudyID,STRING,11,0,,,,STUDYID,STUDYID,Protocol Value,,ENG,Study Identifier,"Although this field is not typically captured on a CRF, it should be displayed clearly on the CRF and/or the EDC system. This field can be included into the database or populated during SDTM-based dataset creation before submission.", -Sponsor,I.RFICDAT,Date informed consent obtained,Date informed consent obtained,DATE,,0,,,,RFICDAT,"DM:RFICDTC|DS:DSSTDTC",Collected Value,,ENG,Informed Consent DATE,This will be the same information on informed consent used in the SDTM Disposition domain, -Sponsor,I.RFICTIM,Time informed consent obtained,Time informed consent obtained,TIME,,0,,,,RFICTIM,"DM:RFICDTC|DS:DSSTDTC",Collected Value,,ENG,Informed Consent time,This will be the same information on informed consent used in the SDTM Disposition domain, -Sponsor,I.RFICDATLAR1,Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated],,DATE,,0,,,,RFICDAT,"RFICDTC, DSSTDTC",Collected Value,,ENG,Informed Consent Date (Legal or authorised representative 1),This will be the same information on informed consent used in the SDTM Disposition domain, -Sponsor,I.RFICTIMLAR1,Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated],,TIME,,0,,,,RFICTIM,"RFICDTC, DSSTDTC",Collected Value,,ENG,Informed Consent Time (Legal or authorised representative 1),This will be the same information on informed consent used in the SDTM Disposition domain, -Sponsor,I.RFICDATLAR2,Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated],,DATE,,0,,,,RFICDAT,"RFICDTC, DSSTDTC",Collected Value,,ENG,Informed Consent DATE (Legal or authorised representative 2),This will be the same information on informed consent used in the SDTM Disposition domain, -Sponsor,I.RFICTIMLAR2,Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated],,TIME,,0,,,,RFICTIM,"RFICDTC, DSSTDTC",Collected Value,,ENG,Informed Consent Time (Legal or authorised representative 2),This will be the same information on informed consent used in the SDTM Disposition domain, -Sponsor,I.BRTHDTCARGUS,Date of birth (only for Argus interface) [hidden],DOB,DATE,,0,,,,BRTHDAT,BRTHDTC,Collected Value,,ENG,Date of birth (only for Argus interface) [hidden],, -Sponsor,I.BRTHDTC,Date of birth,DOB,DATE,,0,,,,BRTHDAT,BRTHDTC,Collected Value,,ENG,Date of birth,"If collected, please provide the most complete Date of birth", -Sponsor,I.SEX,Sex [read-only],Sex,STRING,15,0,C66731,C20197|C16576 ,,SEX,SEX,Collected Value,,ENG,Sex [read-only],, -Sponsor,I.SEXDEA,Sex [de-activated],Sex,STRING,15,0,C66731,C20197|C16576 ,,SEX,SEX,Protocol Value,,ENG,Sex [de-activated],'Male' or 'Female' to be defaulted, -Sponsor,I.ETHNIC,Ethnicity,Etnicity,STRING,20,0,C66790,C17459|C41222,,ETHNIC,ETHNIC,Collected Value,,ENG,Ethinicity,, -Sponsor,I.RACE,Race,Race,STRING,40,0,C74457,C41259|C41260|C16352|C41219|C41261,,RACE,RACE,Collected Value,,ENG,Race,, -Sponsor,I.AGE,Age,Age,INTEGER,3,0,,,years,AGE,AGE,Collected Value,,ENG,Age,The Age could be derived from the Date of Birth, -Sponsor,I.RACEOTH,Race other,Race other,STRING,40,0,,,,RACEOTH,RACEOTH in SUPPDM,Collected Value,,ENG,Race other,, -Sponsor,I.SUBJID,Subject No. [read-only],Subject No,STRING,10,0,,,,SUBJID,SUBJID,Collected Value,,ENG,Subject No.,, -Sponsor,I.PREVSUBJ,Previous Subject No.,Previous Subject No,STRING,10,0,,,,PREVSUBJ,PREVSUBJ,Collected Value,,ENG,Previous Subject No.,, -Sponsor,I.VSDAT,Date of examination,Date of examination,DATE,,0,,,,VSDAT,VSDTC,Collected Value,,ENG,Date of examination [de-activated],, -Sponsor,I.SYSBP,Systolic blood pressure,Systolic blood pressure,INTEGER,3,0,,,mmHg,BP_SYSTOLIC,"VSORRES where VSTESTCD=SYSBP, VSORRESU where VSTESTCD=SYSBP",Collected Value,,ENG,Systolic blood pressure,Please collect the systolic blood pressure of the subject, -Sponsor,I.DIABP,Diastolic blood pressure,Diastolic blood pressure,INTEGER,3,0,,,mmHg,BP_DIASTOLIC,"VSORRES where VSTESTCD=DIABP, VSORRESU where VSTESTCD=DIABP",Collected Value,,ENG,Diastolic blood pressure,Please collect the diastolic blood pressure of the subject, -Sponsor,I.PULSE,Pulse,Pulse,INTEGER,3,0,,,beats/min,PULSE,VSORRES/VSORRESU when VSTESTCD=PULSE,Collected Value,,ENG,Pulse,, -Sponsor,I.POSITION,Position,Position,STRING,15,0,C71148,C62122_SITTING|C62167_SUPINE|C62166_STANDING,,POSITION,VSPOS where VSTESTCD=SYSBP | VSPOS where VSTESTCD=DIABP,Collected Value,,ENG,Position of the subject,Please collect the position of the subject during the exam, -Sponsor,I.LATERALITY,Laterality,Laterality,STRING,15,0,C99073,C25228_RIGHT|C25229_LEFT,,LATERALITY,VSLAT where VSTESTCD=SYSBP | VSLAT where VSTESTCD=DIABP,Collected Value,,ENG,Laterality of the measurement,Please collect the laterality of the exam, -Sponsor,I.LOC,Anatomical Location,Anatomical location,STRING,15,0,C74456,C32141_ARM,,LOCATION,VSLOC where VSTESTCD=SYSBP | VSLOC where VSTESTCD=DIABP,Collected Value,,ENG,Anatomical Location of the measurement,Please collect the location of the subject during the exam, \ No newline at end of file +oid,name,prompt,datatype,length,significant_digits,sas_field_name,sds_var_name,origin,comment,unit_definitions,codelist_submission_value,terms,translated_texts,aliases +I.STUDYID,Study ID,StudyID,STRING,11,0,STUDYID,STUDYID,Protocol Value,,,,,"Description::en::Study Identifier||osb:CompletionInstructions::en::Although this field is not typically captured on a CRF, it should be displayed clearly on the CRF and/or the EDC system. This field can be included into the database or populated during SDTM-based dataset creation before submission.", +I.RFICDAT,Date informed consent obtained,Date informed consent obtained,DATE,,0,RFICDAT,DM:RFICDTC|DS:DSSTDTC,Collected Value,,,,,"Description::en::Informed Consent DATE||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICTIM,Time informed consent obtained,Time informed consent obtained,TIME,,0,RFICTIM,DM:RFICDTC|DS:DSSTDTC,Collected Value,,,,,"Description::en::Informed Consent time||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICDATLAR1,Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated],,DATE,,0,RFICDAT,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent Date (Legal or authorised representative 1)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICTIMLAR1,Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated],,TIME,,0,RFICTIM,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent Time (Legal or authorised representative 1)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICDATLAR2,Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated],,DATE,,0,RFICDAT,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent DATE (Legal or authorised representative 2)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICTIMLAR2,Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated],,TIME,,0,RFICTIM,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent Time (Legal or authorised representative 2)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.BRTHDTCARGUS,Date of birth (only for Argus interface) [hidden],DOB,DATE,,0,BRTHDAT,BRTHDTC,Collected Value,,,,,"Description::en::Date of birth (only for Argus interface) [hidden]", +I.BRTHDTC,Date of birth,DOB,DATE,,0,BRTHDAT,BRTHDTC,Collected Value,,,,,"Description::en::Date of birth||osb:CompletionInstructions::en::If collected, please provide the most complete Date of birth", +I.SEX,Sex [read-only],Sex,STRING,15,0,SEX,SEX,Collected Value,,,SEX,M||F,"Description::en::Sex [read-only]", +I.SEXDEA,Sex [de-activated],Sex,STRING,15,0,SEX,SEX,Protocol Value,,,SEX,M||F,"Description::en::Sex [de-activated],'Male' or 'Female' to be defaulted", +I.ETHNIC,Ethnicity,Etnicity,STRING,20,0,ETHNIC,ETHNIC,Collected Value,,,ETHNIC,HISPANIC OR LATINO||NOT HISPANIC OR LATINO,"Description::en::Ethnicity", +I.RACE,Race,Race,STRING,40,0,RACE,RACE,Collected Value,,,RACE,AMERICAN INDIAN OR ALASKA NATIVE||ASIAN||BLACK OR AFRICAN AMERICAN||NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDER||WHITE,"Description::en::Race", +I.AGE,Age,Age,INTEGER,3,0,AGE,AGE,Collected Value,68,,,,"Description::en::Age||osb:CompletionInstructions::en::The Age could be derived from the Date of Birth", +I.RACEOTH,Race other,Race other,STRING,40,0,RACEOTH,RACEOTH in SUPPDM,Collected Value,,,,,"Description::en::Race other", +I.SUBJID,Subject No. [read-only],Subject No,STRING,10,0,SUBJID,SUBJID,Collected Value,,,,,"Description::en::Subject No.", +I.PREVSUBJ,Previous Subject No.,Previous Subject No,STRING,10,0,PREVSUBJ,PREVSUBJ,Collected Value,,,,,"Description::en::Previous Subject No.", +I.VSDAT,Date of examination,Date of examination,DATE,,0,VSDAT,VSDTC,Collected Value,,,,,"Description::en::Date of examination [de-activated]", +I.SYSBP,Systolic blood pressure,Systolic blood pressure,INTEGER,3,,BP_SYSTOLIC,"VSORRES where VSTESTCD=SYSBP, VSORRESU where VSTESTCD=SYSBP",Collected Value,,mmHg,,,"Description::en::Systolic blood pressure||osb:CompletionInstructions::en::Please collect the systolic blood pressure of the subject", +I.DIABP,Diastolic blood pressure,Diastolic blood pressure,INTEGER,3,,BP_DIASTOLIC,"VSORRES where VSTESTCD=DIABP, VSORRESU where VSTESTCD=DIABP",Collected Value,,mmHg||Pa,,,"Description::en::Diastolic blood pressure||osb:CompletionInstructions::en::Please collect the diastolic blood pressure of the subject", +I.PULSE,Pulse,Pulse,INTEGER,3,,PULSE,VSORRES/VSORRESU when VSTESTCD=PULSE,Collected Value,,beats/min,,,"Description::en::Pulse", +I.POSITION,Position,Position,STRING,15,,POSITION,VSPOS where VSTESTCD=SYSBP | VSPOS where VSTESTCD=DIABP,Collected Value,,,POSITION,SITTING||SUPINE||STANDING,"Description::en::Position of the subject||osb:CompletionInstructions::en::Please collect the position of the subject during the exam", +I.LATERALITY,Laterality,Laterality,STRING,15,,LATERALITY,VSLAT where VSTESTCD=SYSBP | VSLAT where VSTESTCD=DIABP,Collected Value,,,LAT,RIGHT||LEFT,"Description::en::Laterality of the measurement||osb:CompletionInstructions::en::Please collect the laterality of the exam", +I.LOC,Anatomical Location,Anatomical location,STRING,15,,LOCATION,VSLOC where VSTESTCD=SYSBP | VSLOC where VSTESTCD=DIABP,Collected Value,,,LOC,ARM,"Description::en::Anatomical Location of the measurement||osb:CompletionInstructions::en::Please collect the location of the subject during the exam", \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv new file mode 100644 index 00000000..779a3b72 --- /dev/null +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv @@ -0,0 +1,3 @@ +item_oid,item_group_oid,form_oid,activity_instance_name,activity_item_class_name,order,primary,preset_response_value,value_condition,value_dependent_map +I.SYSBP,G.VS.BPP,F.VS,Systolic Blood Pressure,standard_unit,999999,No,,, +I.DIABP,G.VS.BPP,F.VS,Diastolic Blood Pressure,standard_unit,999999,No,,, \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv new file mode 100644 index 00000000..08521373 --- /dev/null +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv @@ -0,0 +1,29 @@ +item_oid,item_group_oid,order_number,mandatory,key_sequence,method_oid,imputation_method_oid,role,role_codelist_oid,collection_exception_condition_oid +I.STUDYID,G.DM.IC,1,Yes,,,,,, +I.RFICDAT,G.DM.IC,2,Yes,,,,,, +I.RFICTIM,G.DM.IC,3,Yes,,,,,, +I.RFICDATLAR1,G.DM.IC,4,No,,,,,, +I.RFICTIMLAR1,G.DM.IC,5,No,,,,,, +I.RFICDATLAR2,G.DM.IC,6,No,,,,,, +I.RFICTIMLAR2,G.DM.IC,7,No,,,,,, +I.BRTHDTCARGUS,G.DM.DM,1,Yes,,,,,, +I.BRTHDTC,G.DM.DM,2,Yes,,,,,, +I.AGE,G.DM.DM,3,Yes,,,,,, +I.SEX,G.DM.DM,4,No,,,,,, +I.SEXDEA,G.DM.DM,5,Yes,,,,,, +I.ETHNIC,G.DM.DM,6,Yes,,,,,, +I.RACE,G.DM.DM,7,Yes,,,,,, +I.RACEOTH,G.DM.DM,8,No,,,,,, +I.SUBJID,G.DM.DM,9,Yes,,,,,, +I.PREVSUBJ,G.DM.DM,10,No,,,,,, +I.STUDYID,G.MH.NS,1,Yes,,,,,, +I.SUBJID,G.MH.NS,2,Yes,,,,,, +I.STUDYID,G.VS.VS,1,Yes,,,,,, +I.SUBJID,G.VS.VS,2,Yes,,,,,, +I.VSDAT,G.VS.VS,3,Yes,,,,,, +I.SYSBP,G.VS.BPP,1,Yes,,,,,, +I.DIABP,G.VS.BPP,2,Yes,,,,,, +I.POSITION,G.VS.BPP,3,Yes,,,,,, +I.LATERALITY,G.VS.BPP,4,Yes,,,,,, +I.LOC,G.VS.BPP,5,Yes,,,,,, +I.PULSE,G.VS.BPP,6,Yes,,,,,, \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_study_events.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_study_events.csv new file mode 100644 index 00000000..d0864e28 --- /dev/null +++ b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_study_events.csv @@ -0,0 +1,11 @@ +oid,name,effective_date,retired_date,description,display_in_tree +T.ODM-1-3-2-V1,ODM version 1.3.2 with DoB,2021-01-01,2022-12-31,description for ODM version 1.3.2 with DoB,True +T.ODM-1-3-2-V2,ODM version 1.3.2 with Age,2021-02-01,2022-06-30,description for ODM version 1.3.2 with Age,True +T.ODM.FINDING.ECG,Finding ECG Template,2022-06-17,2022-06-30,description for Finding ECG Template,True +192169_OP9000000002001,NN Veeva Standards Library_TRN1,2025-07-14,,Created by Library Importer,True +Randomisation,Randomisation,,,,True +T.JHNE_OLCIN.1767877066,JOHANNES_COLLECTION,,,,True +Consent to Biosamples for Future Research,Consent to Biosamples for Future Research,,,,True +T.VS,Vital Signs Forms Collection,,,,True +T.LAB,Lab Collections,,,,True +T.BODY_COLL,Body Measures Collection,,,,True diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_templates.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_templates.csv deleted file mode 100644 index 3e6cc6a1..00000000 --- a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_templates.csv +++ /dev/null @@ -1,4 +0,0 @@ -library,oid,name,effectivedate,retireddate -Sponsor,T.ODM-1-3-2-V1,ODM version 1.3.2 with DoB,2021-01-01,2022-12-31 -Sponsor,T.ODM-1-3-2-V2,ODM version 1.3.2 with Age,2021-02-01,2022-06-30 -Sponsor,T.ODM.FINDING.ECG,Finding ECG Template,2022-06-17,2022-06-30 \ No newline at end of file diff --git a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv b/studybuilder-import/datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv deleted file mode 100644 index dce1b5d5..00000000 --- a/studybuilder-import/datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv +++ /dev/null @@ -1,7 +0,0 @@ -oid_template,oid_form,order_number,mandatory,locked,collection_exception_condition_oid -T.ODM-1-3-2-V1,F.DM,1,No,No, -T.ODM-1-3-2-V1,F.VS,1,No,No, -T.ODM-1-3-2-V1,F.MH,1,No,No, -T.ODM-1-3-2-V1,F.IM,1,No,No, -T.ODM-1-3-2-V1,F.AE,1,No,No, -T.ODM-1-3-2-V1,F.EG,1,No,No, \ No newline at end of file diff --git a/studybuilder-import/datafiles/sponsor_library/activity/activity_item_class.csv b/studybuilder-import/datafiles/sponsor_library/activity/activity_item_class.csv index a5d9e3a2..f03b5fa0 100644 --- a/studybuilder-import/datafiles/sponsor_library/activity/activity_item_class.csv +++ b/studybuilder-import/datafiles/sponsor_library/activity/activity_item_class.csv @@ -139,4 +139,11 @@ SubjectVisit,epi_pandemic_change_indicator,Epi/Pandemic Change Indicator,Epi/Pan SubjectVisit,visit_occurence_reason,Visit Occurrence Reason,Reason for occurrence of visit,,,,No,No,Yes,No,3,Yes,RECOQUAL,TEXT, SubjectVisit,visit_start_datetime,Start Date and Time of Visit,Start date/time of the visit,C83436,,,No,No,Yes,No,4,Yes,TIMING,DATETIME, SubjectVisit,visit_end_datetime,End Date and Time of Visit,End date/time of the visit,C83435,,,No,No,Yes,No,5,Yes,TIMING,DATETIME, -SubjectVisit,description_of_unplanned_visit,Description of Unplanned Visit,Description of unplanned visit,C88009,,,No,No,Yes,No,6,Yes,SYNOQUAL,TEXT, \ No newline at end of file +SubjectVisit,description_of_unplanned_visit,Description of Unplanned Visit,Description of unplanned visit,C88009,,,No,No,Yes,No,6,Yes,SYNOQUAL,TEXT, +GeneralObservation,diparmcd,Device Parameters,To specify details of auxiliary device for Standard Program (DEVTYPE MANUF MODEL),,,,,No,No,No,No,8,No,VARIQUAL,TEXT, +GeneralObservation,orresu,Original Result Unit,To specify that collected original result unit to be retained in SDTM for non numeric topic codes,,,,,No,No,No,No,9,No,VARIQUAL,TEXT, +GeneralObservation,sf36,SF36 Category,To indicate category of SF36 collected data,,,,,No,No,No,No,10,No,VARIQUAL,TEXT, +GeneralObservation,stdunit,Standard Unit,To specify standard unit for an intervention topic code,,,,,No,No,No,No,11,No,VARIQUAL,TEXT, +GeneralObservation,__bdagnt,Binding Agent,To specify the submission value for --BDAGNT (Binding agent in CP IS LB domains),,,,,No,No,No,No,12,No,VARIQUAL,TEXT, +GeneralObservation,__tstopo,Test Operational Objective,To specify the submission value for --TSTOPO (Test Operational Objective in IS LB),,,,,No,No,No,No,13,No,VARIQUAL,TEXT, +GeneralObservation,__tmthsn,Test Method Sensitivity,To specify the submission value for --TMTHSN (Test method sensitivity in LB),,,,,No,No,No,No,14,No,VARIQUAL,TEXT, diff --git a/studybuilder-import/datafiles/sponsor_library/lock_study_milestone.csv b/studybuilder-import/datafiles/sponsor_library/lock_study_milestone.csv deleted file mode 100644 index acf28d99..00000000 --- a/studybuilder-import/datafiles/sponsor_library/lock_study_milestone.csv +++ /dev/null @@ -1,7 +0,0 @@ -CODELIST_SUBMVAL,SPONSOR_NAME,SPONSOR_NAME_SENTENCE_CASE,TERM_SUBMVAL,DEFINITION,ORDER,CONCEPT_ID,CATALOGUES,NCI_PREFERRED_NAME -LOCK_STUDY_MILESTONE,PORT Approval,port approval,PORT_APPROVAL,Milestone for locking study after PORT approval,1,,,PORT Approval -LOCK_STUDY_MILESTONE,PORT Submission,port submission,PORT_SUBMISSION,Milestone for locking study after PORT submission,2,,,PORT Submission -LOCK_STUDY_MILESTONE,Protocol QC,protocol qc,PROTOCOL_QC,Milestone for locking study after protocol quality control,3,,,Protocol QC -LOCK_STUDY_MILESTONE,Final Protocol,final protocol,FINAL_PROTOCOL,Milestone for locking study after final protocol completion,4,,,Final Protocol -LOCK_STUDY_MILESTONE,Data Specification Updates,data specification updates,DATA_SPECIFICATION_UPDATES,Milestone for locking study after data specification updates,5,,,Data Specification Updates -LOCK_STUDY_MILESTONE,Other,other,OTHER,Other milestone for locking study,6,,,Other \ No newline at end of file diff --git a/studybuilder-import/datafiles/sponsor_library/reason_for_lock.csv b/studybuilder-import/datafiles/sponsor_library/reason_for_lock.csv new file mode 100644 index 00000000..71aa8c7e --- /dev/null +++ b/studybuilder-import/datafiles/sponsor_library/reason_for_lock.csv @@ -0,0 +1,7 @@ +CODELIST_SUBMVAL,SPONSOR_NAME,SPONSOR_NAME_SENTENCE_CASE,TERM_SUBMVAL,DEFINITION,ORDER,CONCEPT_ID,CATALOGUES,NCI_PREFERRED_NAME +RSNFL,PORT Approval,port approval,PORT_APPROVAL,Milestone for locking study after PORT approval,1,,,PORT Approval +RSNFL,PORT Submission,port submission,PORT_SUBMISSION,Milestone for locking study after PORT submission,2,,,PORT Submission +RSNFL,Protocol QC,protocol qc,PROTOCOL_QC,Milestone for locking study after protocol quality control,3,,,Protocol QC +RSNFL,Final Protocol,final protocol,FINAL_PROTOCOL,Milestone for locking study after final protocol completion,4,,,Final Protocol +RSNFL,Data Specification Updates,data specification updates,DATA_SPECIFICATION_UPDATES,Milestone for locking study after data specification updates,5,,,Data Specification Updates +RSNFL,Other,other,OTHER,Other milestone for locking study,6,,,Other \ No newline at end of file diff --git a/studybuilder-import/datafiles/sponsor_library/reason_for_unlock.csv b/studybuilder-import/datafiles/sponsor_library/reason_for_unlock.csv new file mode 100644 index 00000000..0c1990d8 --- /dev/null +++ b/studybuilder-import/datafiles/sponsor_library/reason_for_unlock.csv @@ -0,0 +1,6 @@ +CODELIST_SUBMVAL,SPONSOR_NAME,SPONSOR_NAME_SENTENCE_CASE,TERM_SUBMVAL,DEFINITION,ORDER,CONCEPT_ID,CATALOGUES,NCI_PREFERRED_NAME +RSNFUL,Updates after Protocol Development Milestone,updates after protocol development milestone,UPDATES_AFTER_PROTOCOL_DEVELOPMENT_MILESTONE,Milestone for unlocking study for updates after protocol development milestone,1,,,Updates after Protocol Development Milestone +RSNFUL,Data Collection Updates,data collection updates,DATA_COLLECTION_UPDATES,Milestone for unlocking study for data collection updates,2,,,Data Collection Updates +RSNFUL,Study Specification Updates,study specification updates,STUDY_SPECIFICATION_UPDATES,Milestone for unlocking study for study specification updates,3,,,Study Specification Updates +RSNFUL,Amendment,amendment,AMENDMENT,Milestone for unlocking study for protocol amendment,4,,,Amendment +RSNFUL,Other,other,OTHER,Other milestone for unlocking study,5,,,Other \ No newline at end of file diff --git a/studybuilder-import/datafiles/sponsor_library/sponsor_codelist_definitions.csv b/studybuilder-import/datafiles/sponsor_library/sponsor_codelist_definitions.csv index 7fc10452..666e0b76 100644 --- a/studybuilder-import/datafiles/sponsor_library/sponsor_codelist_definitions.csv +++ b/studybuilder-import/datafiles/sponsor_library/sponsor_codelist_definitions.csv @@ -65,6 +65,6 @@ Sponsor,SDTM CT,,Category for Exposure,Category for Exposure,N,EXCAT,,Category f Sponsor,SDTM CT,,Category for Substance Use,Category for Substance Use,N,SUCAT,,Category for Substance Use,Controlled Terminology for Category for Substance Use,Y,N,, Sponsor,SDTM CT,,Category for Vital Signs,Category for Vital Signs,N,VSCAT,,Category for Vital Signs,Controlled Terminology for Category for Vital Signs,Y,N,, Sponsor,SDTM CT,,Data Supplier Type,Data Supplier Type,N,DATA_SUPPLIER_TYPE,,Data Supplier Type,Categorisation of data suppliers into different type of systems or other types of data suppliers,Y,N,, -Sponsor,SDTM CT,,,Lock Study Milestone,N,LOCK_STUDY_MILESTONE,,Lock Study Milestone,Milestones for locking study at different stages of development,Y,N,, -Sponsor,SDTM CT,,,Unlock Study Milestone,N,UNLOCK_STUDY_MILESTONE,,Unlock Study Milestone,Milestones for unlocking study for various updates and modifications,Y,N,, +Sponsor,SDTM CT,,,Reason For Lock,N,RSNFL,,Reason For Lock,Milestones for locking study at different stages of development,Y,N,, +Sponsor,SDTM CT,,,Reason For Unlock,N,RSNFUL,,Reason For Unlock,Milestones for unlocking study for various updates and modifications,Y,N,, Sponsor,SDTM CT,,,Development Stage,N,DEVELOPMENT_STAGE,,Development Stage,Development stage codelist to support device studies,Y,N,, diff --git a/studybuilder-import/datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv b/studybuilder-import/datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv new file mode 100644 index 00000000..0a063901 --- /dev/null +++ b/studybuilder-import/datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv @@ -0,0 +1,2 @@ +activity_item_class,codelist +categoric_finding_original_result,HEP_CQ;NY \ No newline at end of file diff --git a/studybuilder-import/datafiles/sponsor_library/unlock_study_milestone.csv b/studybuilder-import/datafiles/sponsor_library/unlock_study_milestone.csv deleted file mode 100644 index 04bc3dac..00000000 --- a/studybuilder-import/datafiles/sponsor_library/unlock_study_milestone.csv +++ /dev/null @@ -1,6 +0,0 @@ -CODELIST_SUBMVAL,SPONSOR_NAME,SPONSOR_NAME_SENTENCE_CASE,TERM_SUBMVAL,DEFINITION,ORDER,CONCEPT_ID,CATALOGUES,NCI_PREFERRED_NAME -UNLOCK_STUDY_MILESTONE,Updates after Protocol Development Milestone,updates after protocol development milestone,UPDATES_AFTER_PROTOCOL_DEVELOPMENT_MILESTONE,Milestone for unlocking study for updates after protocol development milestone,1,,,Updates after Protocol Development Milestone -UNLOCK_STUDY_MILESTONE,Data Collection Updates,data collection updates,DATA_COLLECTION_UPDATES,Milestone for unlocking study for data collection updates,2,,,Data Collection Updates -UNLOCK_STUDY_MILESTONE,Study Specification Updates,study specification updates,STUDY_SPECIFICATION_UPDATES,Milestone for unlocking study for study specification updates,3,,,Study Specification Updates -UNLOCK_STUDY_MILESTONE,Amendment,amendment,AMENDMENT,Milestone for unlocking study for protocol amendment,4,,,Amendment -UNLOCK_STUDY_MILESTONE,Other,other,OTHER,Other milestone for unlocking study,5,,,Other \ No newline at end of file diff --git a/studybuilder-import/doc/how_to_customize_sponsor_model.md b/studybuilder-import/doc/how_to_customize_sponsor_model.md new file mode 100644 index 00000000..6429102e --- /dev/null +++ b/studybuilder-import/doc/how_to_customize_sponsor_model.md @@ -0,0 +1,398 @@ +# Dynamic Fields Implementation for Sponsor Model Import + +## Overview + +This implementation adds a flexible, maintainable system for handling CSV imports with both expected and dynamic fields. The system automatically transforms CSV data into API-ready payloads while allowing new columns to be added to CSVs without code changes. + +This is useful for evolution of the sponsor model across versions ; and to support various implementations of sponsor models across sponsor companies. + +## Architecture + +### 1. Property Definition System + +The core of the system consists of three main components: + +#### `PropertyType` (Enum) +Defines the types of transformations available: +- `STRING` - String field (empty strings become None) +- `BOOLEAN` - Parse "Y"/"X"/"True"/"true"/"1"/"Yes" becomes True ; everything else becomes False +- `REVERSE_BOOLEAN` - Parse and invert boolean +- `INTEGER` - Convert to integer +- `LIST_SPACE_SEPARATED` - Split string by spaces +- `CUSTOM` - Use custom transformer function + +#### `PropertyDefinition` (Dataclass) +Defines how a CSV field maps to an API field: +- `csv_field`: Column name in CSV +- `api_field`: Field name in API request body +- `property_type`: Type of transformation to apply +- `required`: Whether field must be present +- `custom_transformer`: Optional custom function for CUSTOM property type +- `default_value`: Default if field missing +- `conditional_check`: Function to determine if field should be processed + +#### `FieldMapper` (Class) +Maps CSV rows to API bodies: +- Holds reference to parser instance for access to parsing methods +- Contains transformation functions for each PropertyType +- Automatically applies correct transformers based on property definitions +- Includes dynamic fields not in property definitions + +### 2. Pre-defined Property Definitions + +#### Dataset Properties +Defined in `get_dataset_property_definitions()`: +- **Required fields**: Table, Label, Class, enrich_build_order, basic_std, comment +- **Optional fields**: XmlPath, XmlTitle, Structure, Purpose, Keys, SortKeys, etc. +- **Conditional fields**: isnotcdiscstd, cdiscstd (processed based on CSV headers) + +#### Dataset Variable Properties +Defined in `get_dataset_variable_property_definitions()`: +- **Required fields**: table, column, class_table, class_column, order, basic_std +- **Optional fields**: label, type, length, displayformat, xmldatatype, etc. +- **Conditional fields**: origintype, originsource, isnotcdiscstd + +## Usage + +### Adding a New Expected Field + +To add a new expected field with transformation: + +```python +# In get_dataset_property_definitions() or get_dataset_variable_property_definitions() +PropertyDefinition( + csv_field="NewCSVColumn", + api_field="new_api_field", + property_type=PropertyType.BOOLEAN, # or STRING, LIST_SPACE_SEPARATED, etc. + required=False, # Set to True if mandatory +) +``` + +### Custom Transformations + +For complex transformations, use a custom transformer: + +```python +PropertyDefinition( + csv_field="ComplexField", + api_field="complex_field", + property_type=PropertyType.CUSTOM, + custom_transformer=lambda v: my_complex_transformation(v), +) +``` + +### Conditional Fields + +For fields that should only be processed if certain conditions are met: + +```python +PropertyDefinition( + csv_field="OptionalColumn", + api_field="optional_field", + property_type=PropertyType.STRING, + conditional_check=lambda headers: "OptionalColumn" in headers, +) +``` + +## How It Works + +### Step-by-Step Process + +1. **CSV is opened** and headers are read +2. **Property definitions are loaded** for the entity type (dataset or dataset_variable) +3. **For each row**: + a. For each property definition: + - Check if conditional check passes (if defined) + - Check if field exists in CSV headers + - Extract value from row + - Apply appropriate transformer based on PropertyType + - Add transformed value to `body` dict + + b. After processing all defined properties: + - Scan headers for any columns not in property definitions + - Add these as dynamic `string` fields (sanitized key, empty → None) + + c. Add common body parameters (sponsor_model_name, etc.) + + d. Send to API + +### Dynamic Field Handling + +Any CSV column that is NOT in the property definitions is automatically: +- **Sanitized**: Lowercase, spaces/hyphens → underscores +- **Null-handled**: Empty strings converted to None +- **Passed through**: Sent to API as-is (probably string or integer) + +This means you can add a new column like "custom_metadata" to your CSV and it will automatically be included in the API request without any code changes. + +### Field Handling - API side + +Ensure that the called API will handle your object correctly. The initial implementation proposed by OpenStudyBuilder ensures: + +* Expected fields are handled specifically +* Dynamic fields will be "passed through", meaning no business rule will be applied to them and they will be stored "as-is" + +## Examples + +### Example 1: Standard Field with Transformation + +```python +# CSV has column "include_in_raw" with values "Y" or "" +PropertyDefinition( + csv_field="include_in_raw", + api_field="include_in_raw", + property_type=PropertyType.BOOLEAN, # Automatically converts Y → True, "" → None +) +``` + +### Example 2: Field with Custom Transformation + +```python +# CSV has "Class" column that needs special parsing +PropertyDefinition( + csv_field="Class", + api_field="implemented_dataset_class", + property_type=PropertyType.CUSTOM, + required=True, + custom_transformer=lambda v: parser_instance.parse_dataset_class_name( + v, row_context.get("Table", None) # Access other row fields via row_context + ), +) +``` + +### Example 3: Conditional Field + +```python +# Only process "isnotcdiscstd" if it exists in CSV, otherwise use "basic_std" +PropertyDefinition( + csv_field="isnotcdiscstd", + api_field="is_cdisc_std", + property_type=PropertyType.REVERSE_BOOLEAN, + conditional_check=lambda headers: "isnotcdiscstd" in headers, +), +PropertyDefinition( + csv_field="basic_std", + api_field="is_cdisc_std", + property_type=PropertyType.BOOLEAN, + conditional_check=lambda headers: "isnotcdiscstd" not in headers, +) +``` + +### Example 4: Dynamic Field Automatically Included + +```csv +Table,Label,Class,basic_std,comment,custom-sponsor-flag +AE,Adverse Events,Events,Y,Standard AE domain,SPONSOR_SPECIFIC +``` + +The `custom-sponsor-flag` column will automatically be: +- Detected as a dynamic field (not in property definitions) +- Sanitized to `custom_sponsor_flag` +- Sent to API as `{"custom_sponsor_flag": "SPONSOR_SPECIFIC"}` + +## Migration Path + +### For Existing CSVs (provided by Novo Nordisk) +All existing CSVs will work without changes. The system handles: +- All currently expected fields via property definitions +- Any existing extra columns for future versions as dynamic fields + +### For New Columns +Simply add the column to your CSV: +1. **If transformation needed**: Add PropertyDefinition to the appropriate function +2. **If no transformation needed**: You can just add to CSV, it will be auto-included. Ideally, reference it in the PropertyDefinition list and in the API for better control. + +## Testing Recommendations + +### Test Cases to Validate + +1. **Standard import**: All expected fields present +2. **Missing optional fields**: Ensure defaults work correctly +3. **Extra dynamic fields**: Add unknown columns, verify they're passed through +4. **Conditional fields**: Test with/without conditional columns +5. **Edge cases**: Empty strings, null values, special characters in dynamic field names + +## Summary + +This implementation provides a robust, flexible system for handling CSV imports with: +- ✅ Centralized field definitions +- ✅ Automatic type transformations +- ✅ Support for dynamic/unknown fields +- ✅ Conditional field handling +- ✅ Easy maintenance and extension +- ✅ Backward compatibility with existing CSVs +- ✅ Forward compatibility with API changes + +The system eliminates the need to update import scripts every time a new column is added to CSVs, while maintaining type safety and transformation logic for known fields. + +---------- + +## Property Definition System - Quick Reference + +## Adding a New Expected Field + +### List Field (Space-Separated) + +```python +PropertyDefinition( + csv_field="Tags", # CSV: "tag1 tag2 tag3" + api_field="tags", # API: ["tag1", "tag2", "tag3"] + property_type=PropertyType.LIST_SPACE_SEPARATED, +) +``` + +### Custom Transformation + +```python +PropertyDefinition( + csv_field="ComplexField", + api_field="complex_field", + property_type=PropertyType.CUSTOM, + custom_transformer=lambda v: your_custom_function(v), +) +``` + +### Custom with Access to Other Row Fields + +```python +PropertyDefinition( + csv_field="Field1", + api_field="field1", + property_type=PropertyType.CUSTOM, + custom_transformer=lambda v: process_with_context( + v, + row_context.get("Field2") # Access other field from same row + ), +) +``` + +## Field Options + +### Required Field + +```python +PropertyDefinition( + csv_field="MandatoryColumn", + api_field="mandatory_field", + property_type=PropertyType.STRING, + required=True, # Will raise error if missing from CSV +) +``` + +### Field with Default Value + +```python +PropertyDefinition( + csv_field="OptionalColumn", + api_field="optional_field", + property_type=PropertyType.STRING, + default_value="default_value", # Used if field exists in CSV but cell is empty +) +``` + +### Conditional Field + +```python +PropertyDefinition( + csv_field="RareColumn", + api_field="rare_field", + property_type=PropertyType.STRING, + conditional_check=lambda headers: "OtherRareColumn" in headers, +) +``` + +### Mutually Exclusive Conditional Fields + +```python +# Use Field1 if present, otherwise use Field2 +PropertyDefinition( + csv_field="Field1", + api_field="result", + property_type=PropertyType.STRING, + conditional_check=lambda headers: "Field1" in headers, +), +PropertyDefinition( + csv_field="Field2", + api_field="result", + property_type=PropertyType.STRING, + conditional_check=lambda headers: "Field1" not in headers, +) +``` + +## Where to Add Definitions + +### For Dataset Fields +Edit the list returned by `get_dataset_property_definitions()` in [run_import_sponsormodels.py](run_import_sponsormodels.py) + +### For Dataset Variable Fields +Edit the list returned by `get_dataset_variable_property_definitions()` in [run_import_sponsormodels.py](run_import_sponsormodels.py) + +## Common Mistakes to Avoid + +❌ **Don't do this**: +```python +# Missing custom_transformer when using PropertyType.CUSTOM +PropertyDefinition( + csv_field="Field", + api_field="field", + property_type=PropertyType.CUSTOM, # ERROR: Will raise ValueError +) +``` + +✅ **Do this instead**: +```python +PropertyDefinition( + csv_field="Field", + api_field="field", + property_type=PropertyType.CUSTOM, + custom_transformer=lambda v: transform(v), # Required! +) +``` + +--- + +❌ **Don't do this**: +```python +# Accessing row fields directly in custom_transformer +PropertyDefinition( + csv_field="Field1", + api_field="field1", + property_type=PropertyType.CUSTOM, + custom_transformer=lambda v: combine(v, row[headers.index("Field2")]), # ERROR +) +``` + +✅ **Do this instead**: +```python +PropertyDefinition( + csv_field="Field1", + api_field="field1", + property_type=PropertyType.CUSTOM, + custom_transformer=lambda v: combine(v, row_context.get("Field2")), # Use row_context +) +``` + +--- + +❌ **Don't do this**: +```python +# Duplicate api_field without conditional_check +PropertyDefinition(csv_field="Field1", api_field="result", ...), +PropertyDefinition(csv_field="Field2", api_field="result", ...), # Both will be processed! +``` + +✅ **Do this instead**: +```python +PropertyDefinition( + csv_field="Field1", + api_field="result", + conditional_check=lambda headers: "Field1" in headers, +), +PropertyDefinition( + csv_field="Field2", + api_field="result", + conditional_check=lambda headers: "Field1" not in headers, # Mutually exclusive +) +``` + + diff --git a/studybuilder-import/e2e_datafiles/configuration/feature_flags.csv b/studybuilder-import/e2e_datafiles/configuration/feature_flags.csv index e5dbd997..0a4aa6c9 100644 --- a/studybuilder-import/e2e_datafiles/configuration/feature_flags.csv +++ b/studybuilder-import/e2e_datafiles/configuration/feature_flags.csv @@ -3,8 +3,12 @@ studies_view_listings_analysis_study_metadata_new,1,"This flag toggles on/off th new_activity_instance_wizard_stepper,1,"This flag toggles on/off the new wizard stepper to create/edit Activity Instances (Library/Concepts/Activities/Activity Instances)" activity_instance_wizard_stepper_categoric_findings,0,"This flag toggles on/off the Categoric Findings use case in the new wizard stepper for activity instances" activity_instance_wizard_stepper_textual_findings,0,"This flag toggles on/off the Textual Findings use case in the new wizard stepper for activity instances" +activity_instance_wizard_stepper_edit_mode,0,"This flag toggles on/off the edit mode of the new wizard stepper for activity instances" compounds_library,0,"This flag toggles on/off the whole subpage found at Library/Concepts/Compounds" compounds_studies,0,"This flag toggles on/off the whole subpage found at Studies/Define Study/Study Interventions" complexity_score_calculation,0,"Enables complexity score calculation on the Detailed SoA page" study_data_suppliers,1,"This flag toggles on/off the Study Data Suppliers page found at Studies/Manage/Study Data Suppliers" study_data_suppliers_create_from_study,1,"This flag toggles on/off the ability to create a user-defined data supplier from study level (Add a user defined data supplier button)" +prodex_extension,0,"Show/hide admin menu for managing extracts and loads of ProdEx data into OSB" +meddra_dictionary,0,"This flag toggles on/off the MedDRA dictionary page found at Library/Dictionaries/MedDRA" +loinc_dictionary,0,"This flag toggles on/off the LOINC dictionary page found at Library/Dictionaries/LOINC" \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms.csv index 0dcd57b9..9efcf224 100644 --- a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms.csv +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms.csv @@ -1,24 +1,24 @@ -library,oid,name,prompt,repeating,language,description,instruction,aliases -Sponsor,F.DM,Informed Consent and Demography,,false,ENG,Informed Consent and Demography form,"Please complete this Informed Consent and Demography form at the very beginning of the study - -General item design notes: -Integration: A: Argus, Ax: Forms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM - -General item design notes: Integration: A: Argus, Ax: - -rms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM - -Oracle item des -N notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/DemogOracle item design notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/Demog", -Sponsor,F.VS,Vital Signs,,false,ENG,Vital signs form,Please complete this Vital Sign form before starting the treatment, -Sponsor,F.MH,Medical History/Concomitant Illness,,false,ENG,Medical History/Concomitant Illness (Without pre-printed diagnosis),"This CRF is to be used for studies not collecting specific medical histories (e.g. Cardiovascular History, Diabetes History, Gallbladder Disease History).", -Sponsor,F.IM,Administration of ,,false,ENG,Administration of ,"In case of multiple investigational medicinal products, there should be separate CRFs for each, unless the study in blinded. -In case the study is blinded, the investigational medical product should be named with combination of the drugs with a '/' between the names. -The items: ‘Dose form’ and ‘Route’ do not need to be visible for investigator if only one response is used, but values must be available in the data loaded. -If stop date is not collected, copy start date (ECSTDTC) to stop date (ECENDTC). This could be cases for administrations considered given at a point in time (e.g., oral tablet, pre-filled syringe injection). -Either Prescribed dose or Actual dose has to be collected together with the Start date. -This form is a maximum content form and no additional questions and/or response options can be added unless evaluated E2E by IG and approved by the PST.", -Sponsor,F.AE,Adverse Event,,false,ENG,Adverse Event,"One AE should be reported per form., -During conduct of the study, please transcribe data to EDC as soon as possible. -The AE diagnosis, causality, seriousness and severity should be evaluated by the investigator or sub-investigator with physician background.", -Sponsor,F.EG,ECG,,false,ENG,ECG form,Please complete this ECG form before starting the treatment, \ No newline at end of file +oid,name,repeating,sdtm_version,translated_texts,aliases +F.DM,Informed Consent and Demography,No,,"Description::en::Informed Consent and Demography form||osb:CompletionInstructions::en::Please complete this Informed Consent and Demography form at the very beginning of the study + +General item design notes: +Integration: A: Argus, Ax: Forms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM + +General item design notes: Integration: A: Argus, Ax: + +rms attached in Argus, C: CPR Dashboard, IW: IWRS, P: Impact, R: Reports, RT: RTSM + +Oracle item des +N notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/DemogOracle item design notes: Key: [*] = Item is required. Sex: Populated by IWRS. Item to trigger Childbearing potential form to appear if response = Female. Subject No.: Populated by IWRS and mapped from ENR to Inf Cons/Demog", +F.VS,Vital Signs,No,,"Description::en::Vital signs form||osb:CompletionInstructions::en::Please complete this Vital Sign form before starting the treatment", +F.MH,Medical History/Concomitant Illness,No,,"Description::en::Medical History/Concomitant Illness (Without pre-printed diagnosis)||osb:CompletionInstructions::en::This CRF is to be used for studies not collecting specific medical histories (e.g. Cardiovascular History, Diabetes History, Gallbladder Disease History).", +F.IM,Administration of ,No,,"Description::en::Administration of ||osb:CompletionInstructions::en::In case of multiple investigational medicinal products, there should be separate CRFs for each, unless the study in blinded. +In case the study is blinded, the investigational medical product should be named with combination of the drugs with a '/' between the names. +The items: ‘Dose form’ and ‘Route’ do not need to be visible for investigator if only one response is used, but values must be available in the data loaded. +If stop date is not collected, copy start date (ECSTDTC) to stop date (ECENDTC). This could be cases for administrations considered given at a point in time (e.g., oral tablet, pre-filled syringe injection). +Either Prescribed dose or Actual dose has to be collected together with the Start date. +This form is a maximum content form and no additional questions and/or response options can be added unless evaluated E2E by IG and approved by the PST.", +F.AE,Adverse Event,No,,"Description::en::Adverse Event||osb:CompletionInstructions::en::One AE should be reported per form., +During conduct of the study, please transcribe data to EDC as soon as possible. +The AE diagnosis, causality, seriousness and severity should be evaluated by the investigator or sub-investigator with physician background.", +F.EG,ECG,No,,"Description::en::ECG form||osb:CompletionInstructions::en::Please complete this ECG form before starting the treatment", \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv deleted file mode 100644 index ddf2d5a9..00000000 --- a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms_to_itemgroups.csv +++ /dev/null @@ -1,7 +0,0 @@ -uid_form,oid_form,uid_itemgroup,oid_itemgroup,order_number,mandatory,collection_exception_condition_oid -F.DM,F.DM,G.DM.IC,G.DM.IC,1,TRUE, -F.DM,F.DM,G.DM.DM,G.DM.DM,2,TRUE, -F.MH,F.MH,G.MH.NS,G.MH.NS,1,TRUE, -F.MH,F.MH,G.MH.CM,G.MH.CM,2,TRUE, -F.VS,F.VS,G.VS.VS,G.VS.VS,1,TRUE, -F.VS,F.VS,G.VS.BPP,G.VS.BPP,2,TRUE, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv new file mode 100644 index 00000000..bd290945 --- /dev/null +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_forms_to_odm_study_events.csv @@ -0,0 +1,7 @@ +form_oid,study_event_oid,order_number,mandatory,locked,collection_exception_condition_oid +F.DM,T.ODM-1-3-2-V1,1,No,No, +F.VS,T.ODM-1-3-2-V1,1,No,No, +F.MH,T.ODM-1-3-2-V1,1,No,No, +F.IM,T.ODM-1-3-2-V1,1,No,No, +F.AE,T.ODM-1-3-2-V1,1,No,No, +F.EG,T.ODM-1-3-2-V1,1,No,No, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_item_groups.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_item_groups.csv new file mode 100644 index 00000000..45d9ae68 --- /dev/null +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_item_groups.csv @@ -0,0 +1,8 @@ +oid,name,repeating,is_reference_data,sas_dataset_name,origin,purpose,comment,sdtm_domains,translated_texts,aliases +G.DM.IC,Informed Consent,No,No,DEMOG,Collected Value,Tabulation,,DM||DS,"Description::en::Informed Consent item group||osb:CompletionInstructions::en::Please complete the Informed Consent item group before any other information", +G.DM.DM,General Demography,No,No,DEMOG,Collected Value,Tabulation,,DM,"Description::en::General Demographic item group||osb:CompletionInstructions::en::Please complete this General Demographic item group at the very beginning of the study", +G.MH.NS,Any conditions / illnesses,No,No,MEDHIS,Collected Value,Tabulation,,MH,"Description::en::Any conditions / illnesses ?||osb:CompletionInstructions::en::Please state if there was any conditions / Illnesses", +G.MH.CM,Medical History / Concomitant Illness,Yes,No,MEDHIS,Collected Value,Tabulation,,MH,"Description::en::Medical History item group||osb:CompletionInstructions::en::Please complete this Medical History item group before starting the treatment", +G.VS.VS,Vital Signs,Yes,No,VITALSIGNS,Collected Value,Tabulation,,VS,"Description::en::Vital signs||osb:CompletionInstructions::en::Please complete the Vital Signs item group at each expected time point", +G.VS.BPP,Blood pressure and pulse,No,No,VITALSIGNSBPP,Collected Value,Tabulation,,VS,"Description::en::Blood pressure and pulse||osb:CompletionInstructions::en::Please complete the Blood pressure and Pulse item group at each expected time point", +G.EG.ECGSTATUS,Is teh ECG,No,No,ECGSTATUS,Collected Value,Tabulation,,EG,"Description::en::ECG Test Results||osb:CompletionInstructions::en::Please complete the ECG Test Results item group at each expected time point", \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv new file mode 100644 index 00000000..60b3cd42 --- /dev/null +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_item_groups_to_odm_forms.csv @@ -0,0 +1,7 @@ +item_group_oid,form_oid,order_number,mandatory,collection_exception_condition_oid +G.DM.IC,F.DM,1,Yes, +G.DM.DM,F.DM,2,Yes, +G.MH.NS,F.MH,1,Yes, +G.MH.CM,F.MH,2,Yes, +G.VS.VS,F.VS,1,Yes, +G.VS.BPP,F.VS,2,Yes, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_itemgroups.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_itemgroups.csv deleted file mode 100644 index 20e8afa5..00000000 --- a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_itemgroups.csv +++ /dev/null @@ -1,8 +0,0 @@ -library,oid,name,prompt,repeating,isreferencedata,sasdatasetname,domain,origin,purpose,comment,language,description,instruction,aliases -Sponsor,G.DM.IC,Informed Consent,,false,false,DEMOG,"DM|DS",Collected Value,Tabulation,,ENG,Informed Consent item group,Please complete the Informed Consent item group before any other information, -Sponsor,G.DM.DM,General Demography ,,false,false,DEMOG,DM,Collected Value,Tabulation,,ENG,General Demographic item group,Please complete this General Demographic item group at the very beginning of the study, -Sponsor,G.MH.NS,Any conditions / illnesses,,false,false,MEDHIS,MH,Collected Value,Tabulation,,ENG,Any conditions / illnesses ?,Please state if there was any conditions / Illnesses, -Sponsor,G.MH.CM,Medical History / Concomitant Illness,,true,false,MEDHIS,MH,Collected Value,Tabulation,,ENG,Medical History item group,Please complete this Medical History item group before starting the treatment, -Sponsor,G.VS.VS,Vital Signs,,true,false,VITALSIGNS,VS,Collected Value,Tabulation,,ENG,Vital signs,Please complete the Vital Signs item group at each expected time point, -Sponsor,G.VS.BPP,Blood pressure and pulse,,false,false,VITALSIGNSBPP,VS,Collected Value,Tabulation,,ENG,Blood pressure and pulse,Please complete the Blood pressure and Pulse item group at each expected time point, -Sponsor,G.EG.ECGSTATUS,Is teh ECG,,false,false,ECGSTATUS,EG,Collected Value,Tabulation,,ENG,ECG Test Results,Please complete the ECG Test Results item group at each expected time point, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv deleted file mode 100644 index 8105348d..00000000 --- a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_itemgroups_to_items.csv +++ /dev/null @@ -1,9 +0,0 @@ -uid_itemgroup,oid_itemgroup,uid_item,oid_item,order_number,mandatory,key_sequence,method_oid,imputation_method_oid,role,role_codelist_oid,collection_exception_condition_oid -G.DM.IC,G.DM.IC,I.STUDYID,I.STUDYID,1,TRUE,,,,,, -G.DM.IC,G.DM.IC,I.RFICDAT,I.RFICDAT,2,TRUE,,,,,, -G.DM.IC,G.DM.IC,I.RFICTIM,I.RFICTIM,3,TRUE,,,,,, -G.DM.IC,G.DM.IC,I.RFICDATLAR1,I.RFICDATLAR1,4,FALSE,,,,,, -G.DM.IC,G.DM.IC,I.RFICTIMLAR1,I.RFICTIMLAR1,5,FALSE,,,,,, -G.DM.IC,G.DM.IC,I.RFICDATLAR2,I.RFICDATLAR2,6,FALSE,,,,,, -G.DM.IC,G.DM.IC,I.RFICTIMLAR2,I.RFICTIMLAR2,7,FALSE,,,,,, -G.DM.DM,G.DM.DM,I.BRTHDTCARGUS,I.BRTHDTCARGUS,1,TRUE,,,,,, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items.csv index 676ed00c..867c14e5 100644 --- a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items.csv +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items.csv @@ -1,3 +1,25 @@ -library,oid,name,prompt,datatype,length,significantdigits,codelist,term,unit,sasfieldname,sdsvarname,origin,comment,language,description,instruction,aliases -Sponsor,I.STUDYID,Study ID,StudyID,STRING,11,0,,,,STUDYID,STUDYID,Protocol Value,,ENG,Study Identifier,"Although this field is not typically captured on a CRF, it should be displayed clearly on the CRF and/or the EDC system. This field can be included into the database or populated during SDTM-based dataset creation before submission.", -Sponsor,I.RFICDAT,Date informed consent obtained,Date informed consent obtained,DATE,,0,,,,RFICDAT,"DM:RFICDTC|DS:DSSTDTC",Collected Value,,ENG,Informed Consent DATE,This will be the same information on informed consent used in the SDTM Disposition domain, \ No newline at end of file +oid,name,prompt,datatype,length,significant_digits,sas_field_name,sds_var_name,origin,comment,unit_definitions,codelist_submission_value,terms,translated_texts,aliases +I.STUDYID,Study ID,StudyID,STRING,11,0,STUDYID,STUDYID,Protocol Value,,,,,"Description::en::Study Identifier||osb:CompletionInstructions::en::Although this field is not typically captured on a CRF, it should be displayed clearly on the CRF and/or the EDC system. This field can be included into the database or populated during SDTM-based dataset creation before submission.", +I.RFICDAT,Date informed consent obtained,Date informed consent obtained,DATE,,0,RFICDAT,DM:RFICDTC|DS:DSSTDTC,Collected Value,,,,,"Description::en::Informed Consent DATE||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICTIM,Time informed consent obtained,Time informed consent obtained,TIME,,0,RFICTIM,DM:RFICDTC|DS:DSSTDTC,Collected Value,,,,,"Description::en::Informed Consent time||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICDATLAR1,Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) [de-activated],,DATE,,0,RFICDAT,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent Date (Legal or authorised representative 1)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICTIMLAR1,Informed Consent TIME obtained by Parents/Legally Acceptable Representative (LAR) [de-activated],,TIME,,0,RFICTIM,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent Time (Legal or authorised representative 1)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICDATLAR2,Date informed consent obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated],,DATE,,0,RFICDAT,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent DATE (Legal or authorised representative 2)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.RFICTIMLAR2,Informed Consent Time obtained by Parents/Legally Acceptable Representative (LAR) Only to be completed in countries where Informed Consent from both parents is required [de-activated],,TIME,,0,RFICTIM,"RFICDTC, DSSTDTC",Collected Value,,,,,"Description::en::Informed Consent Time (Legal or authorised representative 2)||osb:CompletionInstructions::en::This will be the same information on informed consent used in the SDTM Disposition domain", +I.BRTHDTCARGUS,Date of birth (only for Argus interface) [hidden],DOB,DATE,,0,BRTHDAT,BRTHDTC,Collected Value,,,,,"Description::en::Date of birth (only for Argus interface) [hidden]", +I.BRTHDTC,Date of birth,DOB,DATE,,0,BRTHDAT,BRTHDTC,Collected Value,,,,,"Description::en::Date of birth||osb:CompletionInstructions::en::If collected, please provide the most complete Date of birth", +I.SEX,Sex [read-only],Sex,STRING,15,0,SEX,SEX,Collected Value,,,SEX,M||F,"Description::en::Sex [read-only]", +I.SEXDEA,Sex [de-activated],Sex,STRING,15,0,SEX,SEX,Protocol Value,,,SEX,M||F,"Description::en::Sex [de-activated],'Male' or 'Female' to be defaulted", +I.ETHNIC,Ethnicity,Etnicity,STRING,20,0,ETHNIC,ETHNIC,Collected Value,,,ETHNIC,HISPANIC OR LATINO||NOT HISPANIC OR LATINO,"Description::en::Ethnicity", +I.RACE,Race,Race,STRING,40,0,RACE,RACE,Collected Value,,,RACE,AMERICAN INDIAN OR ALASKA NATIVE||ASIAN||BLACK OR AFRICAN AMERICAN||NATIVE HAWAIIAN OR OTHER PACIFIC ISLANDER||WHITE,"Description::en::Race", +I.AGE,Age,Age,INTEGER,3,0,AGE,AGE,Collected Value,68,,,,"Description::en::Age||osb:CompletionInstructions::en::The Age could be derived from the Date of Birth", +I.RACEOTH,Race other,Race other,STRING,40,0,RACEOTH,RACEOTH in SUPPDM,Collected Value,,,,,"Description::en::Race other", +I.SUBJID,Subject No. [read-only],Subject No,STRING,10,0,SUBJID,SUBJID,Collected Value,,,,,"Description::en::Subject No.", +I.PREVSUBJ,Previous Subject No.,Previous Subject No,STRING,10,0,PREVSUBJ,PREVSUBJ,Collected Value,,,,,"Description::en::Previous Subject No.", +I.VSDAT,Date of examination,Date of examination,DATE,,0,VSDAT,VSDTC,Collected Value,,,,,"Description::en::Date of examination [de-activated]", +I.SYSBP,Systolic blood pressure,Systolic blood pressure,INTEGER,3,,BP_SYSTOLIC,"VSORRES where VSTESTCD=SYSBP, VSORRESU where VSTESTCD=SYSBP",Collected Value,,mmHg,,,"Description::en::Systolic blood pressure||osb:CompletionInstructions::en::Please collect the systolic blood pressure of the subject", +I.DIABP,Diastolic blood pressure,Diastolic blood pressure,INTEGER,3,,BP_DIASTOLIC,"VSORRES where VSTESTCD=DIABP, VSORRESU where VSTESTCD=DIABP",Collected Value,,mmHg||Pa,,,"Description::en::Diastolic blood pressure||osb:CompletionInstructions::en::Please collect the diastolic blood pressure of the subject", +I.PULSE,Pulse,Pulse,INTEGER,3,,PULSE,VSORRES/VSORRESU when VSTESTCD=PULSE,Collected Value,,beats/min,,,"Description::en::Pulse", +I.POSITION,Position,Position,STRING,15,,POSITION,VSPOS where VSTESTCD=SYSBP | VSPOS where VSTESTCD=DIABP,Collected Value,,,POSITION,SITTING||SUPINE||STANDING,"Description::en::Position of the subject||osb:CompletionInstructions::en::Please collect the position of the subject during the exam", +I.LATERALITY,Laterality,Laterality,STRING,15,,LATERALITY,VSLAT where VSTESTCD=SYSBP | VSLAT where VSTESTCD=DIABP,Collected Value,,,LAT,RIGHT||LEFT,"Description::en::Laterality of the measurement||osb:CompletionInstructions::en::Please collect the laterality of the exam", +I.LOC,Anatomical Location,Anatomical location,STRING,15,,LOCATION,VSLOC where VSTESTCD=SYSBP | VSLOC where VSTESTCD=DIABP,Collected Value,,,LOC,ARM,"Description::en::Anatomical Location of the measurement||osb:CompletionInstructions::en::Please collect the location of the subject during the exam", \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv new file mode 100644 index 00000000..779a3b72 --- /dev/null +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items_to_activity_instances.csv @@ -0,0 +1,3 @@ +item_oid,item_group_oid,form_oid,activity_instance_name,activity_item_class_name,order,primary,preset_response_value,value_condition,value_dependent_map +I.SYSBP,G.VS.BPP,F.VS,Systolic Blood Pressure,standard_unit,999999,No,,, +I.DIABP,G.VS.BPP,F.VS,Diastolic Blood Pressure,standard_unit,999999,No,,, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv new file mode 100644 index 00000000..08521373 --- /dev/null +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_items_to_odm_item_groups.csv @@ -0,0 +1,29 @@ +item_oid,item_group_oid,order_number,mandatory,key_sequence,method_oid,imputation_method_oid,role,role_codelist_oid,collection_exception_condition_oid +I.STUDYID,G.DM.IC,1,Yes,,,,,, +I.RFICDAT,G.DM.IC,2,Yes,,,,,, +I.RFICTIM,G.DM.IC,3,Yes,,,,,, +I.RFICDATLAR1,G.DM.IC,4,No,,,,,, +I.RFICTIMLAR1,G.DM.IC,5,No,,,,,, +I.RFICDATLAR2,G.DM.IC,6,No,,,,,, +I.RFICTIMLAR2,G.DM.IC,7,No,,,,,, +I.BRTHDTCARGUS,G.DM.DM,1,Yes,,,,,, +I.BRTHDTC,G.DM.DM,2,Yes,,,,,, +I.AGE,G.DM.DM,3,Yes,,,,,, +I.SEX,G.DM.DM,4,No,,,,,, +I.SEXDEA,G.DM.DM,5,Yes,,,,,, +I.ETHNIC,G.DM.DM,6,Yes,,,,,, +I.RACE,G.DM.DM,7,Yes,,,,,, +I.RACEOTH,G.DM.DM,8,No,,,,,, +I.SUBJID,G.DM.DM,9,Yes,,,,,, +I.PREVSUBJ,G.DM.DM,10,No,,,,,, +I.STUDYID,G.MH.NS,1,Yes,,,,,, +I.SUBJID,G.MH.NS,2,Yes,,,,,, +I.STUDYID,G.VS.VS,1,Yes,,,,,, +I.SUBJID,G.VS.VS,2,Yes,,,,,, +I.VSDAT,G.VS.VS,3,Yes,,,,,, +I.SYSBP,G.VS.BPP,1,Yes,,,,,, +I.DIABP,G.VS.BPP,2,Yes,,,,,, +I.POSITION,G.VS.BPP,3,Yes,,,,,, +I.LATERALITY,G.VS.BPP,4,Yes,,,,,, +I.LOC,G.VS.BPP,5,Yes,,,,,, +I.PULSE,G.VS.BPP,6,Yes,,,,,, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_study_events.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_study_events.csv new file mode 100644 index 00000000..d0864e28 --- /dev/null +++ b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_study_events.csv @@ -0,0 +1,11 @@ +oid,name,effective_date,retired_date,description,display_in_tree +T.ODM-1-3-2-V1,ODM version 1.3.2 with DoB,2021-01-01,2022-12-31,description for ODM version 1.3.2 with DoB,True +T.ODM-1-3-2-V2,ODM version 1.3.2 with Age,2021-02-01,2022-06-30,description for ODM version 1.3.2 with Age,True +T.ODM.FINDING.ECG,Finding ECG Template,2022-06-17,2022-06-30,description for Finding ECG Template,True +192169_OP9000000002001,NN Veeva Standards Library_TRN1,2025-07-14,,Created by Library Importer,True +Randomisation,Randomisation,,,,True +T.JHNE_OLCIN.1767877066,JOHANNES_COLLECTION,,,,True +Consent to Biosamples for Future Research,Consent to Biosamples for Future Research,,,,True +T.VS,Vital Signs Forms Collection,,,,True +T.LAB,Lab Collections,,,,True +T.BODY_COLL,Body Measures Collection,,,,True diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_templates.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_templates.csv deleted file mode 100644 index 3e6cc6a1..00000000 --- a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_templates.csv +++ /dev/null @@ -1,4 +0,0 @@ -library,oid,name,effectivedate,retireddate -Sponsor,T.ODM-1-3-2-V1,ODM version 1.3.2 with DoB,2021-01-01,2022-12-31 -Sponsor,T.ODM-1-3-2-V2,ODM version 1.3.2 with Age,2021-02-01,2022-06-30 -Sponsor,T.ODM.FINDING.ECG,Finding ECG Template,2022-06-17,2022-06-30 \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv b/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv deleted file mode 100644 index dce1b5d5..00000000 --- a/studybuilder-import/e2e_datafiles/libraries/concepts/crfs/odm_templates_to_forms.csv +++ /dev/null @@ -1,7 +0,0 @@ -oid_template,oid_form,order_number,mandatory,locked,collection_exception_condition_oid -T.ODM-1-3-2-V1,F.DM,1,No,No, -T.ODM-1-3-2-V1,F.VS,1,No,No, -T.ODM-1-3-2-V1,F.MH,1,No,No, -T.ODM-1-3-2-V1,F.IM,1,No,No, -T.ODM-1-3-2-V1,F.AE,1,No,No, -T.ODM-1-3-2-V1,F.EG,1,No,No, \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/sponsor_library/lock_study_milestone.csv b/studybuilder-import/e2e_datafiles/sponsor_library/lock_study_milestone.csv index acf28d99..71aa8c7e 100644 --- a/studybuilder-import/e2e_datafiles/sponsor_library/lock_study_milestone.csv +++ b/studybuilder-import/e2e_datafiles/sponsor_library/lock_study_milestone.csv @@ -1,7 +1,7 @@ CODELIST_SUBMVAL,SPONSOR_NAME,SPONSOR_NAME_SENTENCE_CASE,TERM_SUBMVAL,DEFINITION,ORDER,CONCEPT_ID,CATALOGUES,NCI_PREFERRED_NAME -LOCK_STUDY_MILESTONE,PORT Approval,port approval,PORT_APPROVAL,Milestone for locking study after PORT approval,1,,,PORT Approval -LOCK_STUDY_MILESTONE,PORT Submission,port submission,PORT_SUBMISSION,Milestone for locking study after PORT submission,2,,,PORT Submission -LOCK_STUDY_MILESTONE,Protocol QC,protocol qc,PROTOCOL_QC,Milestone for locking study after protocol quality control,3,,,Protocol QC -LOCK_STUDY_MILESTONE,Final Protocol,final protocol,FINAL_PROTOCOL,Milestone for locking study after final protocol completion,4,,,Final Protocol -LOCK_STUDY_MILESTONE,Data Specification Updates,data specification updates,DATA_SPECIFICATION_UPDATES,Milestone for locking study after data specification updates,5,,,Data Specification Updates -LOCK_STUDY_MILESTONE,Other,other,OTHER,Other milestone for locking study,6,,,Other \ No newline at end of file +RSNFL,PORT Approval,port approval,PORT_APPROVAL,Milestone for locking study after PORT approval,1,,,PORT Approval +RSNFL,PORT Submission,port submission,PORT_SUBMISSION,Milestone for locking study after PORT submission,2,,,PORT Submission +RSNFL,Protocol QC,protocol qc,PROTOCOL_QC,Milestone for locking study after protocol quality control,3,,,Protocol QC +RSNFL,Final Protocol,final protocol,FINAL_PROTOCOL,Milestone for locking study after final protocol completion,4,,,Final Protocol +RSNFL,Data Specification Updates,data specification updates,DATA_SPECIFICATION_UPDATES,Milestone for locking study after data specification updates,5,,,Data Specification Updates +RSNFL,Other,other,OTHER,Other milestone for locking study,6,,,Other \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/sponsor_library/sponsor_codelist_definitions.csv b/studybuilder-import/e2e_datafiles/sponsor_library/sponsor_codelist_definitions.csv index 7fc10452..666e0b76 100644 --- a/studybuilder-import/e2e_datafiles/sponsor_library/sponsor_codelist_definitions.csv +++ b/studybuilder-import/e2e_datafiles/sponsor_library/sponsor_codelist_definitions.csv @@ -65,6 +65,6 @@ Sponsor,SDTM CT,,Category for Exposure,Category for Exposure,N,EXCAT,,Category f Sponsor,SDTM CT,,Category for Substance Use,Category for Substance Use,N,SUCAT,,Category for Substance Use,Controlled Terminology for Category for Substance Use,Y,N,, Sponsor,SDTM CT,,Category for Vital Signs,Category for Vital Signs,N,VSCAT,,Category for Vital Signs,Controlled Terminology for Category for Vital Signs,Y,N,, Sponsor,SDTM CT,,Data Supplier Type,Data Supplier Type,N,DATA_SUPPLIER_TYPE,,Data Supplier Type,Categorisation of data suppliers into different type of systems or other types of data suppliers,Y,N,, -Sponsor,SDTM CT,,,Lock Study Milestone,N,LOCK_STUDY_MILESTONE,,Lock Study Milestone,Milestones for locking study at different stages of development,Y,N,, -Sponsor,SDTM CT,,,Unlock Study Milestone,N,UNLOCK_STUDY_MILESTONE,,Unlock Study Milestone,Milestones for unlocking study for various updates and modifications,Y,N,, +Sponsor,SDTM CT,,,Reason For Lock,N,RSNFL,,Reason For Lock,Milestones for locking study at different stages of development,Y,N,, +Sponsor,SDTM CT,,,Reason For Unlock,N,RSNFUL,,Reason For Unlock,Milestones for unlocking study for various updates and modifications,Y,N,, Sponsor,SDTM CT,,,Development Stage,N,DEVELOPMENT_STAGE,,Development Stage,Development stage codelist to support device studies,Y,N,, diff --git a/studybuilder-import/e2e_datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv b/studybuilder-import/e2e_datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv new file mode 100644 index 00000000..0a063901 --- /dev/null +++ b/studybuilder-import/e2e_datafiles/sponsor_library/sponsormodel/activity_item_class_valid_codelists.csv @@ -0,0 +1,2 @@ +activity_item_class,codelist +categoric_finding_original_result,HEP_CQ;NY \ No newline at end of file diff --git a/studybuilder-import/e2e_datafiles/sponsor_library/unlock_study_milestone.csv b/studybuilder-import/e2e_datafiles/sponsor_library/unlock_study_milestone.csv index 04bc3dac..0c1990d8 100644 --- a/studybuilder-import/e2e_datafiles/sponsor_library/unlock_study_milestone.csv +++ b/studybuilder-import/e2e_datafiles/sponsor_library/unlock_study_milestone.csv @@ -1,6 +1,6 @@ CODELIST_SUBMVAL,SPONSOR_NAME,SPONSOR_NAME_SENTENCE_CASE,TERM_SUBMVAL,DEFINITION,ORDER,CONCEPT_ID,CATALOGUES,NCI_PREFERRED_NAME -UNLOCK_STUDY_MILESTONE,Updates after Protocol Development Milestone,updates after protocol development milestone,UPDATES_AFTER_PROTOCOL_DEVELOPMENT_MILESTONE,Milestone for unlocking study for updates after protocol development milestone,1,,,Updates after Protocol Development Milestone -UNLOCK_STUDY_MILESTONE,Data Collection Updates,data collection updates,DATA_COLLECTION_UPDATES,Milestone for unlocking study for data collection updates,2,,,Data Collection Updates -UNLOCK_STUDY_MILESTONE,Study Specification Updates,study specification updates,STUDY_SPECIFICATION_UPDATES,Milestone for unlocking study for study specification updates,3,,,Study Specification Updates -UNLOCK_STUDY_MILESTONE,Amendment,amendment,AMENDMENT,Milestone for unlocking study for protocol amendment,4,,,Amendment -UNLOCK_STUDY_MILESTONE,Other,other,OTHER,Other milestone for unlocking study,5,,,Other \ No newline at end of file +RSNFUL,Updates after Protocol Development Milestone,updates after protocol development milestone,UPDATES_AFTER_PROTOCOL_DEVELOPMENT_MILESTONE,Milestone for unlocking study for updates after protocol development milestone,1,,,Updates after Protocol Development Milestone +RSNFUL,Data Collection Updates,data collection updates,DATA_COLLECTION_UPDATES,Milestone for unlocking study for data collection updates,2,,,Data Collection Updates +RSNFUL,Study Specification Updates,study specification updates,STUDY_SPECIFICATION_UPDATES,Milestone for unlocking study for study specification updates,3,,,Study Specification Updates +RSNFUL,Amendment,amendment,AMENDMENT,Milestone for unlocking study for protocol amendment,4,,,Amendment +RSNFUL,Other,other,OTHER,Other milestone for unlocking study,5,,,Other \ No newline at end of file diff --git a/studybuilder-import/importers/run_import_activities.py b/studybuilder-import/importers/run_import_activities.py index 8121d438..67cd6413 100644 --- a/studybuilder-import/importers/run_import_activities.py +++ b/studybuilder-import/importers/run_import_activities.py @@ -712,16 +712,16 @@ async def handle_activity_instance_class_parent_relationship( existing_rows[current_class_name] = response def are_item_classes_equal(self, new, existing): - def are_instance_classes_covered(): - _new = set() - for i in new.get("activity_instance_classes") or []: - _new.add(i["name"]) - - _existing = set() - for i in existing.get("activity_instance_classes") or []: - _existing.add(i["name"]) - - return _new.issubset(_existing) + def are_instance_classes_equal(): + _new = sorted( + new.get("activity_instance_classes") or [], + key=lambda x: x.get("name", ""), + ) + _existing = sorted( + existing.get("activity_instance_classes") or [], + key=lambda x: x.get("name", ""), + ) + return _new == _existing properties_to_compare = [ "name", @@ -738,15 +738,17 @@ def are_instance_classes_covered(): if self.compare_properties(new, existing, properties_to_compare) is False: self.log.debug("Difference found in basic properties") return False - if not are_instance_classes_covered(): - self.log.debug("Instance classes are not covered") + if not are_instance_classes_equal(): + self.log.debug( + "Instance classes are not equal ; either the list is different, or attached properties are" + ) return False return True @open_file_async() async def handle_activity_item_classes(self, csvfile, session): - # Populate then activity item classes in sponsor library + # Populate the activity item classes in sponsor library readCSV = csv.DictReader(csvfile, delimiter=",") api_tasks = [] diff --git a/studybuilder-import/importers/run_import_crfs.py b/studybuilder-import/importers/run_import_crfs.py index e3302d4d..c7769388 100644 --- a/studybuilder-import/importers/run_import_crfs.py +++ b/studybuilder-import/importers/run_import_crfs.py @@ -1,5 +1,8 @@ import csv import json +from collections import defaultdict + +from importers.functions.parsers import map_boolean from .functions.utils import load_env from .utils.importer import BaseImporter, open_file @@ -12,18 +15,21 @@ API_BASE_URL = load_env("API_BASE_URL") MDR_MIGRATION_ODM_VENDOR_NAMESPACES = load_env("MDR_MIGRATION_ODM_VENDOR_NAMESPACES") MDR_MIGRATION_ODM_VENDOR_ATTRIBUTES = load_env("MDR_MIGRATION_ODM_VENDOR_ATTRIBUTES") -MDR_MIGRATION_ODM_TEMPLATES = load_env("MDR_MIGRATION_ODM_TEMPLATES") +MDR_MIGRATION_ODM_STUDY_EVENTS = load_env("MDR_MIGRATION_ODM_STUDY_EVENTS") MDR_MIGRATION_ODM_FORMS = load_env("MDR_MIGRATION_ODM_FORMS") -MDR_MIGRATION_ODM_ITEMGROUPS = load_env("MDR_MIGRATION_ODM_ITEMGROUPS") +MDR_MIGRATION_ODM_ITEM_GROUPS = load_env("MDR_MIGRATION_ODM_ITEM_GROUPS") MDR_MIGRATION_ODM_ITEMS = load_env("MDR_MIGRATION_ODM_ITEMS") -MDR_MIGRATION_ODM_TEMPLATE_TO_FORM_RELATIONSHIP = load_env( - "MDR_MIGRATION_ODM_TEMPLATE_TO_FORM_RELATIONSHIP" +MDR_MIGRATION_ODM_FORMS_TO_ODM_STUDY_EVENTS = load_env( + "MDR_MIGRATION_ODM_FORMS_TO_ODM_STUDY_EVENTS" +) +MDR_MIGRATION_ODM_ITEM_GROUPS_TO_ODM_FORMS = load_env( + "MDR_MIGRATION_ODM_ITEM_GROUPS_TO_ODM_FORMS" ) -MDR_MIGRATION_ODM_FORM_TO_ITEMGROUP_RELATIONSHIP = load_env( - "MDR_MIGRATION_ODM_FORM_TO_ITEMGROUP_RELATIONSHIP" +MDR_MIGRATION_ODM_ITEMS_TO_ODM_ITEM_GROUPS = load_env( + "MDR_MIGRATION_ODM_ITEMS_TO_ODM_ITEM_GROUPS" ) -MDR_MIGRATION_ODM_ITEMGROUP_TO_ITEM_RELATIONSHIP = load_env( - "MDR_MIGRATION_ODM_ITEMGROUP_TO_ITEM_RELATIONSHIP" +MDR_MIGRATION_ODM_ITEMS_TO_ACTIVITY_INSTANCES = load_env( + "MDR_MIGRATION_ODM_ITEMS_TO_ACTIVITY_INSTANCES" ) @@ -53,70 +59,49 @@ def odm_vendor_attribute(data, vendor_namespace_uid): } -# library,oid,name,effectivedate,retireddate -def odm_template(data): +# library,oid,name,effective_date,retired_date +def odm_study_event(data): return { "path": "/concepts/odms/study-events", "body": { "name": data["name"], - "library_name": data["library"], "oid": data["oid"], - "effective_date": data["effectivedate"], - "retired_date": data["retireddate"], + "effective_date": data["effective_date"] or None, + "retired_date": data["retired_date"] or None, "description": f"description for {data['name']}", }, } -# library,oid,name,prompt,repeating,language,description,instruction +# library,oid,name,prompt,repeating,translated_texts,aliases def odm_form(data): return { "path": "/concepts/odms/forms", "body": { "name": data["name"], - "library_name": data["library"], "oid": data["oid"], - "repeating": "yes" if data["repeating"].lower() == "true" else "no", - "descriptions": [ - { - "name": data["name"], - "language": data["language"], - "description": data["description"] or None, - "instruction": data["instruction"] or None, - "sponsor_instruction": None, - } - ], + "repeating": data["repeating"], + "translated_texts": data.get("translated_texts", []), "aliases": data.get("aliases", []), }, } # library,oid,name,prompt,repeating,isreferencedata,sasdatasetname,domain,origin,purpose,comment,language,description,instruction -def odm_itemgroup(data, domain_uids): +def odm_item_group(data, domain_uids): return { "path": "/concepts/odms/item-groups", "body": { "name": data["name"], - "library_name": data["library"], "oid": data["oid"], - "repeating": "yes" if data["repeating"].lower() == "true" else "no", - "is_reference_data": ( - "yes" if data["isreferencedata"].lower() == "true" else "no" - ), - "sas_dataset_name": data["sasdatasetname"], + "repeating": data["repeating"], + "is_reference_data": data["is_reference_data"], + "sas_dataset_name": data["sas_dataset_name"], "origin": data["origin"], "purpose": data["purpose"], "locked": "no", "comment": data["comment"], - "descriptions": [ - { - "name": data["name"], - "language": data["language"], - "description": data["description"] or None, - "instruction": data["instruction"] or None, - "sponsor_instruction": None, - }, - ], + "translated_texts": data.get("translated_texts", []), "aliases": data.get("aliases", []), "sdtm_domain_uids": domain_uids, }, @@ -129,41 +114,42 @@ def odm_item(data, units, terms): length = int(data["length"]) except ValueError: length = None + try: + significant_digits = int(data["significant_digits"]) + except ValueError: + significant_digits = None return { "path": "/concepts/odms/items", "body": { "name": data["name"], - "library_name": data["library"], "oid": data["oid"], "datatype": data["datatype"], "prompt": data["prompt"], "length": length, - "significant_digits": int(data["significantdigits"]), - "sas_field_name": data["sasfieldname"], - "sds_var_name": data["sdsvarname"], + "significant_digits": significant_digits, + "sas_field_name": data["sas_field_name"], + "sds_var_name": data["sds_var_name"], "origin": data["origin"], "comment": data["comment"], "allows_multi_choice": False, - "descriptions": [ - { - "name": data["name"], - "library_name": data["library"], - "language": data["language"], - "description": data["description"] or None, - "instruction": data["instruction"] or None, - "sponsor_instruction": None, - }, - ], + "translated_texts": data.get("translated_texts", []), "aliases": data.get("aliases", []), - "codelist_uid": data["codelist"] if data["codelist"] != "" else None, + "codelist": ( + { + "uid": data["codelist"]["uid"], + "allows_multi_choice": data["codelist"]["allows_multi_choice"], + } + if data["codelist"] + else None + ), "unit_definitions": units, "terms": terms, }, } -def odm_template_to_form_relationship(uid, data): +def odm_form_to_study_event(uid, data): return { "path": f"/concepts/odms/study-events/{uid}/forms", "body": [ @@ -181,7 +167,7 @@ def odm_template_to_form_relationship(uid, data): } -def odm_form_to_itemgroup_relationship(uid, data): +def odm_item_group_to_form(uid, data): return { "path": f"/concepts/odms/forms/{uid}/item-groups", "body": [ @@ -199,7 +185,7 @@ def odm_form_to_itemgroup_relationship(uid, data): } -def odm_itemgroup_to_item_relationship(uid, data): +def odm_item_to_item_group(uid, data): return { "path": f"/concepts/odms/item-groups/{uid}/items", "body": [ @@ -222,19 +208,31 @@ def odm_itemgroup_to_item_relationship(uid, data): } +def odm_item_to_activity_instance(item): + # odm_item_to_activity_instance_relationship + return { + "path": "/concepts/odms/items/", + "body": item, + } + + class Crfs(BaseImporter): logging_name = "crfs" def __init__(self, api=None, metrics_inst=None): super().__init__(api=api, metrics_inst=metrics_inst) - def _fetch_codelist_terms(self, codelists, codelist): + def _fetch_codelist_terms_and_return_codelist_uid( + self, codelists, codelist_submval + ): + codelist = self.api.get_codelist_uid(codelist_submval) if codelist not in codelists: new_codelist = {} terms = self.api.get_all_from_api(f"/ct/codelists/{codelist}/terms") for term in terms: - new_codelist[term["concept_id"]] = term["term_uid"] + new_codelist[term["submission_value"]] = term["term_uid"] codelists[codelist] = new_codelist + return codelist def _transform_aliases(self, aliases: str | None): rs = [] @@ -242,12 +240,38 @@ def _transform_aliases(self, aliases: str | None): if not aliases: return rs - for alias in aliases.split("|"): - _alias = alias.split(":", maxsplit=1) + for alias in aliases.split("||"): + _alias = alias.split("::", maxsplit=1) if not _alias[0]: continue context, name = _alias - rs.append({"name": name, "context": context}) + rs.append( + { + "name": name, + "context": context, + } + ) + + return rs + + def _transform_translated_texts(self, translated_texts: str | None): + rs = [] + + if not translated_texts: + return rs + + for translated_text in translated_texts.split("||"): + _translated_text = translated_text.split("::", maxsplit=2) + if not _translated_text[0]: + continue + text_type, language, text = _translated_text + rs.append( + { + "text_type": text_type, + "language": language, + "text": text, + } + ) return rs @@ -281,16 +305,16 @@ def handle_odm_vendor_attributes(self, csvfile, vendor_namespace_uid): self.api.post_to_api(data) @open_file() - def handle_odm_templates(self, csvfile): + def handle_odm_study_events(self, csvfile): csvdata = csv.DictReader(csvfile) for row in csvdata: if len(row) == 0: continue - self.log.info(f'Adding odm template {row["name"]}') - data = odm_template(row) + self.log.info(f'Adding odm study event {row["name"]}') + data = odm_study_event(row) - # Create template, and leave in draft state (no approve) + # Create study event, and leave in draft state (no approve) # TODO check if it exists before posting? self.api.post_to_api(data) @@ -304,15 +328,18 @@ def handle_odm_forms(self, csvfile): self.log.info(f'Adding odm form {row["name"]}') row["aliases"] = self._transform_aliases(row["aliases"]) + row["translated_texts"] = self._transform_translated_texts( + row["translated_texts"] + ) data = odm_form(row) - # Create template, and leave in draft state (no approve) + # Create study event, and leave in draft state (no approve) # TODO check if it exists before posting? self.api.post_to_api(data) @open_file() - def handle_odm_itemgroups(self, csvfile): + def handle_odm_item_groups(self, csvfile): csvdata = csv.DictReader(csvfile) params = { "filters": json.dumps( @@ -345,7 +372,7 @@ def handle_odm_itemgroups(self, csvfile): # Look up sdtm domains domains = [] - for domain in row["domain"].split("|"): + for domain in row["sdtm_domains"].split("||"): if not domain: continue domain_uid = all_sdtm_domains.get(domain) @@ -355,10 +382,13 @@ def handle_odm_itemgroups(self, csvfile): self.log.warning(f"Unable to find domain '{domain}'") row["aliases"] = self._transform_aliases(row["aliases"]) + row["translated_texts"] = self._transform_translated_texts( + row["translated_texts"] + ) - data = odm_itemgroup(row, domains) + data = odm_item_group(row, domains) - # Create template, and leave in draft state (no approve) + # Create study event, and leave in draft state (no approve) # TODO check if it exists before posting? self.api.post_to_api(data) @@ -378,15 +408,21 @@ def handle_odm_items(self, csvfile): continue self.log.info(f'Adding odm item {row["name"]}') - codelist = row["codelist"] + row["codelist"] = {} term_dicts = [] - if codelist != "": - self._fetch_codelist_terms(codelists, codelist) - terms = row["term"] + codelist_submission_value = row["codelist_submission_value"] + if codelist_submission_value != "": + row["codelist"]["uid"] = ( + self._fetch_codelist_terms_and_return_codelist_uid( + codelists, codelist_submission_value + ) + ) + row["codelist"]["allows_multi_choice"] = False + terms = row["terms"] if terms != "": - for term in terms.split("|"): + for term in terms.split("||"): term = term.strip().split("_")[0] - term_uid = codelists.get(codelist, {}).get(term) + term_uid = codelists.get(row["codelist"]["uid"], {}).get(term) if term_uid is not None: term_dict = { "uid": term_uid, @@ -396,67 +432,71 @@ def handle_odm_items(self, csvfile): term_dicts.append(term_dict) else: self.log.warning( - f"Unable to find term {term} in codelist {codelist}" + f"Unable to find term {term} in codelist {row["codelist"]["uid"]}" ) units = [] - unit = row["unit"] - if unit != "": - unit_uid = all_units.get(unit) - if unit_uid is not None: - unit_dict = {"uid": unit_uid, "mandatory": True} - units.append(unit_dict) - else: - self.log.warning(f"Unable to find unit {unit}") + unit_definitions = row["unit_definitions"] + if unit_definitions != "": + for unit in unit_definitions.split("||"): + unit_uid = all_units.get(unit) + if unit_uid is not None: + unit_dict = {"uid": unit_uid, "mandatory": True} + units.append(unit_dict) + else: + self.log.warning(f"Unable to find unit {unit}") row["aliases"] = self._transform_aliases(row["aliases"]) + row["translated_texts"] = self._transform_translated_texts( + row["translated_texts"] + ) data = odm_item(row, units, term_dicts) - # Create template, and leave in draft state (no approve) + # Create study event, and leave in draft state (no approve) # TODO check if it exists before posting? self.api.post_to_api(data) @open_file() - def handle_odm_template_to_form_relationship(self, csvfile): + def handle_odm_forms_to_study_events(self, csvfile): csvdata = csv.DictReader(csvfile) odm_forms = self.api.get_all_from_api("/concepts/odms/forms") - odm_templates = self.api.get_all_from_api("/concepts/odms/study-events") + odm_study_events = self.api.get_all_from_api("/concepts/odms/study-events") all_forms = {} for form in odm_forms: all_forms[form["oid"]] = form["uid"] - all_templates = {} - for template in odm_templates: - all_templates[template["oid"]] = template["uid"] + all_study_events = {} + for study_event in odm_study_events: + all_study_events[study_event["oid"]] = study_event["uid"] for row in csvdata: if len(row) == 0: continue self.log.info( - f"Adding odm form '{row['oid_form']}' to template '{row['oid_template']}'" + f"Adding odm form '{row['form_oid']}' to study event '{row['study_event_oid']}'" ) - if row["oid_form"] not in all_forms: - self.log.warning(f"form '{row['oid_form']}' not found, skipping") + if row["form_oid"] not in all_forms: + self.log.warning(f"form '{row['form_oid']}' not found, skipping") continue - if row["oid_template"] not in all_templates: + if row["study_event_oid"] not in all_study_events: self.log.warning( - f"Template '{row['oid_template']}' not found, skipping" + f"Study Event '{row['study_event_oid']}' not found, skipping" ) continue - row["uid"] = all_forms[row["oid_form"]] + row["uid"] = all_forms[row["form_oid"]] - data = odm_template_to_form_relationship( - all_templates[row["oid_template"]], row + data = odm_form_to_study_event( + all_study_events[row["study_event_oid"]], row ) self.api.post_to_api(data) @open_file() - def handle_odm_form_to_itemgroup_relationship(self, csvfile): + def handle_odm_item_groups_to_forms(self, csvfile): csvdata = csv.DictReader(csvfile) odm_itemgroups = self.api.get_all_from_api("/concepts/odms/item-groups") odm_forms = self.api.get_all_from_api("/concepts/odms/forms") @@ -473,26 +513,26 @@ def handle_odm_form_to_itemgroup_relationship(self, csvfile): if len(row) == 0: continue self.log.info( - f"Adding odm item group '{row['oid_itemgroup']}' to form '{row['oid_form']}'" + f"Adding odm item group '{row['item_group_oid']}' to form '{row['form_oid']}'" ) - if row["oid_itemgroup"] not in all_itemgroups: + if row["item_group_oid"] not in all_itemgroups: self.log.warning( - f"Item group '{row['oid_itemgroup']}' not found, skipping" + f"Item group '{row['item_group_oid']}' not found, skipping" ) continue - if row["oid_form"] not in all_forms: - self.log.warning(f"Form '{row['oid_form']}' not found, skipping") + if row["form_oid"] not in all_forms: + self.log.warning(f"Form '{row['form_oid']}' not found, skipping") continue - row["uid"] = all_itemgroups[row["oid_itemgroup"]] + row["uid"] = all_itemgroups[row["item_group_oid"]] - data = odm_form_to_itemgroup_relationship(all_forms[row["oid_form"]], row) + data = odm_item_group_to_form(all_forms[row["form_oid"]], row) self.api.post_to_api(data) @open_file() - def handle_odm_itemgroup_to_item_relationship(self, csvfile): + def handle_odm_items_to_item_groups(self, csvfile): csvdata = csv.DictReader(csvfile) odm_items = self.api.get_all_from_api("/concepts/odms/items") odm_itemgroups = self.api.get_all_from_api("/concepts/odms/item-groups") @@ -509,26 +549,193 @@ def handle_odm_itemgroup_to_item_relationship(self, csvfile): if len(row) == 0: continue self.log.info( - f"Adding odm item '{row['oid_item']}' to item group '{row['oid_itemgroup']}'" + f"Adding odm item '{row['item_oid']}' to item group '{row['item_group_oid']}'" ) - if row["oid_item"] not in all_items: - self.log.warning(f"Item '{row['oid_item']}' not found, skipping") + if row["item_oid"] not in all_items: + self.log.warning(f"Item '{row['item_oid']}' not found, skipping") continue - if row["oid_itemgroup"] not in all_itemgroups: + if row["item_group_oid"] not in all_itemgroups: self.log.warning( - f"Item group '{row['oid_itemgroup']}' not found, skipping" + f"Item group '{row['item_group_oid']}' not found, skipping" ) continue - row["uid"] = all_items[row["oid_item"]] + row["uid"] = all_items[row["item_oid"]] - data = odm_itemgroup_to_item_relationship( - all_itemgroups[row["oid_itemgroup"]], row - ) + data = odm_item_to_item_group(all_itemgroups[row["item_group_oid"]], row) self.api.post_to_api(data) + @open_file() + def handle_odm_items_to_activity_instances(self, csvfile): + csvdata = list(csv.DictReader(csvfile)) + + item_oids = {data["item_oid"] for data in csvdata} + item_group_oids = {data["item_group_oid"] for data in csvdata} + form_oids = {data["form_oid"] for data in csvdata} + activity_instance_names = {data["activity_instance_name"] for data in csvdata} + + odm_items = self.api.get_all_from_api( + "/concepts/odms/items", + params={"filters": json.dumps({"oid": {"v": list(item_oids)}})}, + ) + odm_item_groups = self.api.get_all_from_api( + "/concepts/odms/item-groups", + params={"filters": json.dumps({"oid": {"v": list(item_group_oids)}})}, + ) + odm_forms = self.api.get_all_from_api( + "/concepts/odms/forms", + params={"filters": json.dumps({"oid": {"v": list(form_oids)}})}, + ) + activity_instances = self.api.get_all_from_api( + "/concepts/activities/activity-instances", + params={ + "filters": json.dumps({"name": {"v": list(activity_instance_names)}}) + }, + ) + + data_grouped_by_item = defaultdict(list) + for row in csvdata: + item_oid = row.get("item_oid") + if item_oid: + data_grouped_by_item[item_oid].append(row) + + for item_oid, rows in data_grouped_by_item.items(): + if not ( + item := next((oi for oi in odm_items if oi["oid"] == item_oid), None) + ): + self.log.warning("ODM Item '%s' not found, skipping", item_oid) + continue + + activity_instances_to_add = [] + + for row in rows: + if len(row) == 0: + continue + + self.log.info( + "Connecting ODM Item '%s' of ODM Item Group '%s' of ODM Form '%s' to Activity Instance '%s' with Activity Item Class '%s'", + item_oid, + row["item_group_oid"], + row["form_oid"], + row["activity_instance_name"], + row["activity_item_class_name"], + ) + + if not ( + item_group := next( + ( + oig + for oig in odm_item_groups + if oig["oid"] == row["item_group_oid"] + ), + None, + ) + ): + self.log.warning( + "ODM Item Group '%s' not found, skipping", row["item_group_oid"] + ) + continue + + if item_oid not in [oig["oid"] for oig in item_group["items"]]: + self.log.warning( + "ODM Item '%s' is not part of ODM Item Group '%s', skipping", + item_oid, + row["item_group_oid"], + ) + continue + + if not ( + form := next( + (of for of in odm_forms if of["oid"] == row["form_oid"]), + None, + ) + ): + self.log.warning( + "ODM Form '%s' not found, skipping", row["form_oid"] + ) + continue + + if row["item_group_oid"] not in [ + of["oid"] for of in form["item_groups"] + ]: + self.log.warning( + "ODM Item Group '%s' is not part of ODM Form '%s', skipping", + row["item_group_oid"], + row["form_oid"], + ) + continue + + if not ( + activity_instance := next( + ( + ai + for ai in activity_instances + if ai["name"] == row["activity_instance_name"] + ), + None, + ) + ): + self.log.warning( + "Activity Instance '%s' not found, skipping", + row["activity_instance_name"], + ) + continue + + if not ( + activity_item_class := next( + ( + ai["activity_item_class"] + for ai in activity_instance["activity_items"] + if ai["activity_item_class"]["name"] + == row["activity_item_class_name"] + ), + None, + ) + ): + self.log.warning( + "Activity Item Class '%s' not found in Activity Instance '%s', skipping", + row["activity_item_class_name"], + row["activity_instance_name"], + ) + continue + + activity_instances_to_add.append( + { + "activity_instance_uid": activity_instance["uid"], + "activity_item_class_uid": activity_item_class["uid"], + "odm_form_uid": form["uid"], + "odm_item_group_uid": item_group["uid"], + "order": row["order"] if row["order"].isdigit() else 99999, + "primary": map_boolean(row["primary"]), + "preset_response_value": row["preset_response_value"], + "value_condition": row["value_condition"], + "value_dependent_map": row["value_dependent_map"], + } + ) + + item["codelist"] = ( + { + "uid": item["codelist"].get("uid"), + "allows_multi_choice": item["codelist"].get( + "allows_multi_choice", False + ), + } + if item["codelist"] + else None + ) + + for term in item.get("terms", []): + term["uid"] = term["term_uid"] + + if activity_instances_to_add: + item["activity_instances"] = activity_instances_to_add + + data = odm_item_to_activity_instance(item) + + self.api.patch_to_api(data["body"], data["path"]) + def run(self): self.log.info("Importing CRFs") vendor_namespace_res = self.handle_odm_vendor_namespaces( @@ -543,18 +750,17 @@ def run(self): or "OdmVendorNamespace_000001" ), ) - self.handle_odm_templates(MDR_MIGRATION_ODM_TEMPLATES) + self.handle_odm_study_events(MDR_MIGRATION_ODM_STUDY_EVENTS) self.handle_odm_forms(MDR_MIGRATION_ODM_FORMS) - self.handle_odm_itemgroups(MDR_MIGRATION_ODM_ITEMGROUPS) + self.handle_odm_item_groups(MDR_MIGRATION_ODM_ITEM_GROUPS) self.handle_odm_items(MDR_MIGRATION_ODM_ITEMS) - self.handle_odm_template_to_form_relationship( - MDR_MIGRATION_ODM_TEMPLATE_TO_FORM_RELATIONSHIP - ) - self.handle_odm_form_to_itemgroup_relationship( - MDR_MIGRATION_ODM_FORM_TO_ITEMGROUP_RELATIONSHIP + self.handle_odm_forms_to_study_events( + MDR_MIGRATION_ODM_FORMS_TO_ODM_STUDY_EVENTS ) - self.handle_odm_itemgroup_to_item_relationship( - MDR_MIGRATION_ODM_ITEMGROUP_TO_ITEM_RELATIONSHIP + self.handle_odm_item_groups_to_forms(MDR_MIGRATION_ODM_ITEM_GROUPS_TO_ODM_FORMS) + self.handle_odm_items_to_item_groups(MDR_MIGRATION_ODM_ITEMS_TO_ODM_ITEM_GROUPS) + self.handle_odm_items_to_activity_instances( + MDR_MIGRATION_ODM_ITEMS_TO_ACTIVITY_INSTANCES ) self.log.info("Done importing CRFs") diff --git a/studybuilder-import/importers/run_import_sponsormodels.py b/studybuilder-import/importers/run_import_sponsormodels.py index 03a232e3..db3f11fe 100644 --- a/studybuilder-import/importers/run_import_sponsormodels.py +++ b/studybuilder-import/importers/run_import_sponsormodels.py @@ -4,7 +4,10 @@ import os import re from collections import defaultdict +from dataclasses import dataclass from datetime import datetime +from enum import Enum +from typing import Any, Callable import aiohttp @@ -26,6 +29,9 @@ MDR_MIGRATION_ACTIVITY_ITEM_CLASS_MODEL_RELS = load_env( "MDR_MIGRATION_ACTIVITY_ITEM_CLASS_MODEL_RELS" ) +MDR_MIGRATION_ACTIVITY_ITEM_CLASS_VALID_CODELIST_RELS = load_env( + "MDR_MIGRATION_ACTIVITY_ITEM_CLASS_VALID_CODELIST_RELS" +) MDR_MIGRATION_SPONSOR_MODEL_DIRECTORY = load_env( "MDR_MIGRATION_SPONSOR_MODEL_DIRECTORY" ) @@ -45,7 +51,504 @@ ACTIVITY_ITEM_CLASSES_PATH = "/activity-item-classes" +# --------------------------------------------------------------- +# Property Definition System +# --------------------------------------------------------------- + + +class PropertyType(Enum): + """Type of property transformation needed""" + + STRING = "string" # String field (empty → None) + BOOLEAN = "boolean" # Parse Y/X to bool + REVERSE_BOOLEAN = "reverse_boolean" # Parse and reverse + INTEGER = "integer" # Convert to int + LIST_SPACE_SEPARATED = "list_space_separated" # Split by space + CUSTOM = "custom" # Custom transformer function + + +@dataclass +class PropertyDefinition: + """ + Definition of how to map and transform a CSV field to an API field. + + Attributes: + csv_field: The header name in the CSV file + api_field: The field name to send to the API + property_type: Type of transformation to apply - Defaults to STRING + required: Whether this field must be present in CSV - Defaults to False + custom_transformer: Optional custom transformation function + default_value: Default value when cell is empty (not when header missing) - Defaults to None + conditional_check: Optional function to determine if field should be processed + """ + + csv_field: str + api_field: str + property_type: PropertyType = PropertyType.STRING + required: bool = False + custom_transformer: Callable[[Any], Any] | None = None + default_value: Any = None + conditional_check: Callable[[list[str]], bool] | None = None + + +class FieldMapper: # pylint: disable=too-few-public-methods + """ + Maps CSV fields to API fields with automatic transformation based on PropertyDefinition. + """ + + def __init__(self, parser_instance): + """ + Initialize with reference to parser instance for accessing parsing methods. + + Args: + parser_instance: Instance of SponsorModels class with parsing methods + """ + self.parser = parser_instance + self._transformers = { + PropertyType.STRING: lambda v: v or None, # Empty string becomes None + PropertyType.BOOLEAN: self.parser.parse_bool, + PropertyType.REVERSE_BOOLEAN: lambda v: self.parser.reverse_bool( + self.parser.parse_bool(v) + ), + PropertyType.INTEGER: self.parser.parse_int, + PropertyType.LIST_SPACE_SEPARATED: lambda v: v.split(" ") if v else None, + } + + def map_row_to_body( + self, + headers: list[str], + row: list[str], + property_definitions: list[PropertyDefinition], + include_dynamic_fields: bool = True, + ) -> dict[str, Any]: + """ + Map a CSV row to API body based on property definitions. + + Args: + headers: CSV headers + row: CSV row values + property_definitions: List of property definitions + include_dynamic_fields: Whether to include fields not in definitions - Defaults to True + + Returns: + Dictionary ready to send to API + """ + body = {} + processed_csv_fields = set() + + # Process defined properties + for prop_def in property_definitions: + # Skip if conditional check fails + if prop_def.conditional_check and not prop_def.conditional_check(headers): + continue + + # Check if field exists in CSV headers + if prop_def.csv_field not in headers: + if prop_def.required: + raise ValueError( + f"Required field '{prop_def.csv_field}' not found in CSV headers" + ) + continue + + # Mark as processed + processed_csv_fields.add(prop_def.csv_field) + + # Get value from row + value = row[headers.index(prop_def.csv_field)] + + # Apply default_value if cell is empty and a default is provided + # Note: default_value applies when the CSV header EXISTS but the cell is EMPTY + # This is different from when the header is missing entirely + if not value and prop_def.default_value is not None: + body[prop_def.api_field] = prop_def.default_value + continue + + # Apply transformation + if prop_def.custom_transformer: + transformed_value = prop_def.custom_transformer(value) + elif prop_def.property_type == PropertyType.CUSTOM: + raise ValueError( + f"PropertyType.CUSTOM requires custom_transformer for field '{prop_def.csv_field}'" + ) + else: + transformer = self._transformers[prop_def.property_type] + transformed_value = transformer(value) + + body[prop_def.api_field] = transformed_value + + # Add dynamic fields (not in definitions) + if include_dynamic_fields: + for header in headers: + if header not in processed_csv_fields: + value = row[headers.index(header)] + # Sanitize field name: lowercase, replace spaces with underscores + field_name = header.lower().replace(" ", "_").replace("-", "_") + # Convert empty strings to None + body[field_name] = value if value else None + + return body + + +# --------------------------------------------------------------- +# Dataset Property Definitions +# --------------------------------------------------------------- + + +def get_dataset_property_definitions(parser_instance) -> list[PropertyDefinition]: + """ + Define all property mappings for sponsor model datasets. + + Args: + parser_instance: Instance of SponsorModels for custom transformers + + Returns: + List of PropertyDefinition objects + """ + return [ + # Required fields + PropertyDefinition( + csv_field="Table", + api_field="dataset_uid", + property_type=PropertyType.STRING, + required=True, + ), + PropertyDefinition( + csv_field="Label", + api_field="label", + property_type=PropertyType.STRING, + required=True, + ), + PropertyDefinition( + csv_field="Class", + api_field="implemented_dataset_class", + property_type=PropertyType.CUSTOM, + required=True, + custom_transformer=lambda v: parser_instance.parse_dataset_class_name( + v, row_context.get("Table", None) + ), + ), + PropertyDefinition( + csv_field="enrich_build_order", + api_field="enrich_build_order", + property_type=PropertyType.INTEGER, + required=True, + default_value=0, + ), + PropertyDefinition( + csv_field="basic_std", + api_field="is_basic_std", + property_type=PropertyType.BOOLEAN, + required=True, + ), + PropertyDefinition( + csv_field="comment", + api_field="comment", + property_type=PropertyType.STRING, + required=True, + ), + # Optional fields with standard transformations + PropertyDefinition( + csv_field="XmlPath", + api_field="xml_path", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="XmlTitle", + api_field="xml_title", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="Structure", + api_field="structure", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="Purpose", + api_field="purpose", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="Keys", + api_field="keys", + property_type=PropertyType.LIST_SPACE_SEPARATED, + ), + PropertyDefinition( + csv_field="SortKeys", + api_field="sort_keys", + property_type=PropertyType.LIST_SPACE_SEPARATED, + ), + PropertyDefinition( + csv_field="State", + api_field="state", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="Standardref", + api_field="standard_ref", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="IGcomment", + api_field="ig_comment", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="extended_domain", + api_field="extended_domain", + property_type=PropertyType.STRING, + ), + # Boolean fields + PropertyDefinition( + csv_field="map_domain_flag", + api_field="map_domain_flag", + property_type=PropertyType.BOOLEAN, + ), + PropertyDefinition( + csv_field="suppl_qual_flag", + api_field="suppl_qual_flag", + property_type=PropertyType.BOOLEAN, + ), + PropertyDefinition( + csv_field="include_in_raw", + api_field="include_in_raw", + property_type=PropertyType.BOOLEAN, + ), + PropertyDefinition( + csv_field="gen_raw_seqno_flag", + api_field="gen_raw_seqno_flag", + property_type=PropertyType.BOOLEAN, + ), + # Conditional fields + PropertyDefinition( + csv_field="isnotcdiscstd", + api_field="is_cdisc_std", + property_type=PropertyType.REVERSE_BOOLEAN, + conditional_check=lambda headers: "isnotcdiscstd" in headers, + ), + PropertyDefinition( + csv_field="basic_std", + api_field="is_cdisc_std", + property_type=PropertyType.BOOLEAN, + conditional_check=lambda headers: "isnotcdiscstd" not in headers, + ), + PropertyDefinition( + csv_field="cdiscstd", + api_field="source_ig", + property_type=PropertyType.STRING, + conditional_check=lambda headers: "cdiscstd" in headers, + ), + ] + + +# --------------------------------------------------------------- +# Dataset Variable Property Definitions +# --------------------------------------------------------------- + + +def get_dataset_variable_property_definitions( + parser_instance, +) -> list[PropertyDefinition]: + """ + Define all property mappings for sponsor model dataset variables. + + Args: + parser_instance: Instance of SponsorModels for custom transformers + + Returns: + List of PropertyDefinition objects + """ + return [ + # Required fields + PropertyDefinition( + csv_field="table", + api_field="dataset_uid", + property_type=PropertyType.STRING, + required=True, + ), + PropertyDefinition( + csv_field="column", + api_field="dataset_variable_uid", + property_type=PropertyType.STRING, + required=True, + ), + PropertyDefinition( + csv_field="class_table", + api_field="implemented_parent_dataset_class", + property_type=PropertyType.CUSTOM, + required=True, + custom_transformer=lambda v: parser_instance.parse_dataset_class_name( + class_name=v + ), + ), + PropertyDefinition( + csv_field="class_column", + api_field="implemented_variable_class", + property_type=PropertyType.CUSTOM, + required=True, + custom_transformer=parser_instance.parse_variable_class_name, + ), + PropertyDefinition( + csv_field="order", + api_field="order", + property_type=PropertyType.INTEGER, + required=True, + ), + PropertyDefinition( + csv_field="basic_std", + api_field="is_basic_std", + property_type=PropertyType.BOOLEAN, + required=True, + ), + # Optional fields + PropertyDefinition( + csv_field="label", + api_field="label", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="type", + api_field="variable_type", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="length", + api_field="length", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="displayformat", + api_field="display_format", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="xmldatatype", + api_field="xml_datatype", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="core", + api_field="core", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="origin", + api_field="origin", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="origintype", + api_field="origin_type", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="originsource", + api_field="origin_source", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="role", + api_field="role", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="term", + api_field="term", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="algorithm", + api_field="algorithm", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="qualifiers", + api_field="qualifiers", + property_type=PropertyType.LIST_SPACE_SEPARATED, + ), + PropertyDefinition( + csv_field="comment", + api_field="comment", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="IGcomment", + api_field="ig_comment", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="map_var_flag", + api_field="map_var_flag", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="fixed_mapping", + api_field="fixed_mapping", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="include_in_raw", + api_field="include_in_raw", + property_type=PropertyType.BOOLEAN, + ), + PropertyDefinition( + csv_field="nn_internal", + api_field="nn_internal", + property_type=PropertyType.BOOLEAN, + ), + PropertyDefinition( + csv_field="value_lvl_where_cols", + api_field="value_lvl_where_cols", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="value_lvl_label_col", + api_field="value_lvl_label_col", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="value_lvl_collect_ct_val", + api_field="value_lvl_collect_ct_val", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="value_lvl_ct_cdlist_id_col", + api_field="value_lvl_ct_codelist_id_col", + property_type=PropertyType.STRING, + ), + PropertyDefinition( + csv_field="enrich_build_order", + api_field="enrich_build_order", + property_type=PropertyType.INTEGER, + default_value=0, + ), + PropertyDefinition( + csv_field="enrich_rule", + api_field="enrich_rule", + property_type=PropertyType.STRING, + ), + # Conditional fields + PropertyDefinition( + csv_field="isnotcdiscstd", + api_field="is_cdisc_std", + property_type=PropertyType.REVERSE_BOOLEAN, + conditional_check=lambda headers: "isnotcdiscstd" in headers, + ), + PropertyDefinition( + csv_field="basic_std", + api_field="is_cdisc_std", + property_type=PropertyType.BOOLEAN, + conditional_check=lambda headers: "isnotcdiscstd" not in headers, + ), + ] + + +# Global context for row data (used in custom transformers) +row_context = {} + + +# --------------------------------------------------------------- # SponsorModels with datasets and variables +# --------------------------------------------------------------- class SponsorModels(BaseImporter): logging_name = "sponsor_models" @@ -60,11 +563,14 @@ def __init__(self, api=None, metrics_inst=None): else None ) + # Initialize field mapper for CSV to API transformations + self.field_mapper = FieldMapper(self) + def parse_bool(self, cell: str | None) -> bool | None: if cell is None: return None else: - return cell in ["Y", "X"] + return cell in ["Y", "X", "true", "True", "Yes", "1"] def reverse_bool(self, boolean: bool | None) -> bool | None: if boolean is None: @@ -72,6 +578,14 @@ def reverse_bool(self, boolean: bool | None) -> bool | None: else: return False if boolean else True + def parse_int(self, cell: str | None) -> int | None: + if cell is None: + return None + try: + return int(cell) + except (ValueError, TypeError): + return None + def parse_instance_class_name(self, name: str) -> str: parsed = name.replace("AP ", "AssociatedPersons") return parsed @@ -82,6 +596,22 @@ def parse_item_class_name(self, name: str) -> str: def parse_variable_class_name(self, name: str) -> str: return name.replace("__", "--") + def parse_valid_codelist_uids(self, name: str) -> list[str]: + return name.split(";") + + # Get a dictionary with key = submission value and value = uid + def _get_codelists_uid_and_submval(self): + all_codelist_attributes = self.api.get_all_from_api("/ct/codelists/attributes") + + all_codelist_uids = CaselessDict( + self.api.get_all_identifiers( + all_codelist_attributes, + identifier="submission_value", + value="codelist_uid", + ) + ) + return all_codelist_uids + @open_file_async() async def handle_activity_instance_class_relations( self, instance_class_csvfile, session @@ -194,6 +724,57 @@ async def handle_activity_item_class_relations(self, item_class_csvfile, session # Finally, push all tasks await asyncio.gather(*api_tasks) + @open_file_async() + async def handle_activity_item_class_valid_codelist_relations( + self, item_class_csvfile, session + ): + api_tasks = [] + + # Parse and PATCH ActivityItemClasses + csv_reader = csv.reader(item_class_csvfile, delimiter=",") + headers = next(csv_reader) + existing_item_classes = self.api.get_all_identifiers( + self.api.get_all_from_api(ACTIVITY_ITEM_CLASSES_PATH), + identifier="name", + value="uid", + ) + + existing_item_classes = { + self.parse_item_class_name(i): v for i, v in existing_item_classes.items() + } + + all_codelist_uids = self._get_codelists_uid_and_submval() + for row in csv_reader: + class_cell = row[headers.index("activity_item_class")] + if class_cell: + activity_item_class_uid = existing_item_classes.get( + self.parse_item_class_name(class_cell), + None, + ) + if activity_item_class_uid is not None: + codelist_names = self.parse_valid_codelist_uids( + row[headers.index("codelist")] + ) + codelist_uids = [ + all_codelist_uids.get(codelist_submval) + for codelist_submval in codelist_names + ] + data = {"body": {"valid_codelist_uids": codelist_uids}} + self.log.info( + "Adding relationships to valid codelists for activity item class '%s'", + activity_item_class_uid, + ) + api_tasks.append( + self.api.patch_to_api_async( + path=f"{ACTIVITY_ITEM_CLASSES_PATH}/{activity_item_class_uid}/valid-codelist-mappings", + body=data["body"], + session=session, + ) + ) + + # Finally, push all tasks + await asyncio.gather(*api_tasks) + async def handle_sponsor_model(self, session) -> bool: existing_sponsor_models = self.api.get_all_identifiers( self.api.get_all_from_api(SPONSOR_MODELS_PATH), @@ -274,74 +855,39 @@ def parse_dataset_class_name( @open_file_async() async def handle_datasets(self, csvfile, session): - # Populate sponsor model datasets + """ + Populate sponsor model datasets using the field mapper system. + + This method: + - Automatically maps CSV fields to API fields using PropertyDefinition + - Applies appropriate transformations based on property types + - Includes any extra CSV columns not in the property definitions + """ csv_reader = csv.reader(csvfile, delimiter=",") headers = next(csv_reader) api_tasks = [] + # Get property definitions for datasets + property_definitions = get_dataset_property_definitions(self) + for row in csv_reader: - data = { - "body": { - # Expected fields - "dataset_uid": row[headers.index("Table")], - "implemented_dataset_class": self.parse_dataset_class_name( - row[headers.index("Class")], row[headers.index("Table")] - ), - "enrich_build_order": ( - row[headers.index("enrich_build_order")] - if row[headers.index("enrich_build_order")] - else 0 - ), - # Optional/Changeable fields - # Update in the API if renamed or new fields are added - "is_basic_std": self.parse_bool(row[headers.index("basic_std")]), - "label": row[headers.index("Label")], - "xml_path": row[headers.index("XmlPath")], - "xml_title": row[headers.index("XmlTitle")], - "structure": row[headers.index("Structure")], - "purpose": row[headers.index("Purpose")], - "keys": ( - row[headers.index("Keys")].split(" ") - if row[headers.index("Keys")] - else None - ), - "sort_keys": ( - row[headers.index("SortKeys")].split(" ") - if row[headers.index("SortKeys")] - else None - ), - "state": row[headers.index("State")], - "is_cdisc_std": ( - self.reverse_bool( - self.parse_bool(row[headers.index("isnotcdiscstd")]) - ) - if "isnotcdiscstd" in headers - else self.parse_bool(row[headers.index("basic_std")]) - ), - "source_ig": ( - row[headers.index("cdiscstd")] - if "cdiscstd" in headers - else None - ), - "standard_ref": row[headers.index("Standardref")] or None, - "comment": row[headers.index("comment")] or None, - "ig_comment": row[headers.index("IGcomment")], - "map_domain_flag": self.parse_bool( - row[headers.index("map_domain_flag")] - ), - "suppl_qual_flag": self.parse_bool( - row[headers.index("suppl_qual_flag")] - ), - "include_in_raw": self.parse_bool( - row[headers.index("include_in_raw")] - ), - "gen_raw_seqno_flag": self.parse_bool( - row[headers.index("gen_raw_seqno_flag")] - ), - "extended_domain": row[headers.index("extended_domain")], - **self._common_body_params, - }, - } + # Store row context for custom transformers that need it (e.g., parse_dataset_class_name) + global row_context # pylint: disable=global-statement + row_context = {headers[i]: row[i] for i in range(len(headers))} + + # Map CSV row to API body using field mapper + body = self.field_mapper.map_row_to_body( + headers=headers, + row=row, + property_definitions=property_definitions, + include_dynamic_fields=True, # Include any extra CSV columns + ) + + # Add common parameters + body.update(self._common_body_params) + + data = {"body": body} + self.log.info( f"Add sponsor model dataset '{data['body']['dataset_uid']}' to sponsor model '{data['body']['sponsor_model_name']}'" ) @@ -358,7 +904,7 @@ async def handle_datasets(self, csvfile, session): if data["body"]["is_basic_std"] is False: term_data = { "body": { - "catalogue_name": "SDTM CT", + "catalogue_names": ["SDTM CT"], "codelist_uid": "C66734", "code_submission_value": data["body"]["dataset_uid"], "name_submission_value": data["body"]["label"], @@ -432,95 +978,51 @@ def parse_referenced_ct(self, headers, row, all_codelist_uids) -> tuple[dict, di @open_file_async() async def handle_dataset_variables(self, csvfile, session): - # Populate sponsor model dataset variables + """ + Populate sponsor model dataset variables using the field mapper system. + + This method: + - Automatically maps CSV fields to API fields using PropertyDefinition + - Applies appropriate transformations based on property types + - Includes any extra CSV columns not in the property definitions + - Handles special CT references separately (not in property definitions) + """ csv_reader = csv.reader(csvfile, delimiter=",") headers = next(csv_reader) api_tasks = [] all_codelist_uids = self.api.get_codelists_uid_and_submval() + # Get property definitions for dataset variables + property_definitions = get_dataset_variable_property_definitions(self) + for row in csv_reader: + # Store row context for custom transformers + global row_context # pylint: disable=global-statement + row_context = {headers[i]: row[i] for i in range(len(headers))} + + # Parse CT references (special handling not in property definitions) references_codelists, references_terms = self.parse_referenced_ct( headers=headers, row=row, all_codelist_uids=all_codelist_uids ) - data = { - "body": { - # Expected fields - "dataset_uid": row[headers.index("table")], - "dataset_variable_uid": row[headers.index("column")], - "implemented_parent_dataset_class": self.parse_dataset_class_name( - class_name=row[headers.index("class_table")], - ), - "implemented_variable_class": self.parse_variable_class_name( - row[headers.index("class_column")] - ), - "order": row[headers.index("order")], - # Optional/Changeable fields - # Update in the API if renamed or new fields are added - "is_basic_std": self.parse_bool(row[headers.index("basic_std")]), - "label": row[headers.index("label")], - "variable_type": row[headers.index("type")], - "length": row[headers.index("length")], - "display_format": row[headers.index("displayformat")] or None, - "xml_datatype": row[headers.index("xmldatatype")], - "references_codelists": references_codelists, - "references_terms": references_terms, - "core": row[headers.index("core")], - "origin": row[headers.index("origin")] or None, - "origin_type": ( - row[headers.index("origintype")] or None - if "origintype" in headers - else None - ), - "origin_source": ( - row[headers.index("originsource")] or None - if "originsource" in headers - else None - ), - "role": row[headers.index("role")], - "term": row[headers.index("term")] or None, - "algorithm": row[headers.index("algorithm")] or None, - "qualifiers": ( - row[headers.index("qualifiers")].split(" ") or None - if row[headers.index("qualifiers")] - else None - ), - "is_cdisc_std": ( - self.reverse_bool( - self.parse_bool(row[headers.index("isnotcdiscstd")]) - ) - if "isnotcdiscstd" in headers - else self.parse_bool(row[headers.index("basic_std")]) - ), - "comment": row[headers.index("comment")] or None, - "ig_comment": row[headers.index("IGcomment")] or None, - "map_var_flag": row[headers.index("map_var_flag")], - "fixed_mapping": row[headers.index("fixed_mapping")] or None, - "include_in_raw": self.parse_bool( - row[headers.index("include_in_raw")] - ), - "nn_internal": self.parse_bool(row[headers.index("nn_internal")]), - "value_lvl_where_cols": row[headers.index("value_lvl_where_cols")] - or None, - "value_lvl_label_col": row[headers.index("value_lvl_label_col")] - or None, - "value_lvl_collect_ct_val": row[ - headers.index("value_lvl_collect_ct_val") - ] - or None, - "value_lvl_ct_codelist_id_col": row[ - headers.index("value_lvl_ct_cdlist_id_col") - ] - or None, - "enrich_build_order": ( - row[headers.index("enrich_build_order")] or None - if row[headers.index("enrich_build_order")] - else 0 - ), - "enrich_rule": row[headers.index("enrich_rule")] or None, - **self._common_body_params, - }, - } + + # Map CSV row to API body using field mapper + body = self.field_mapper.map_row_to_body( + headers=headers, + row=row, + property_definitions=property_definitions, + include_dynamic_fields=True, # Include any extra CSV columns + ) + + # Add CT references (not handled by property definitions) + body["references_codelists"] = references_codelists + body["references_terms"] = references_terms + + # Add common parameters + body.update(self._common_body_params) + + data = {"body": body} + self.log.info( f"Add sponsor model variable '{data['body']['dataset_variable_uid']}' to dataset '{data['body']['dataset_uid']}'" ) @@ -546,6 +1048,10 @@ async def async_run(self): MDR_MIGRATION_ACTIVITY_ITEM_CLASS_MODEL_RELS, session, ) + await self.handle_activity_item_class_valid_codelist_relations( + MDR_MIGRATION_ACTIVITY_ITEM_CLASS_VALID_CODELIST_RELS, + session, + ) # For each subfolder in the sponsor models folder, import the corresponding sponsor model for root, dirs, _ in os.walk(MDR_MIGRATION_SPONSOR_MODEL_DIRECTORY): diff --git a/studybuilder-import/importers/run_import_standardcodelistterms1.py b/studybuilder-import/importers/run_import_standardcodelistterms1.py index 1cc9d097..02c01194 100644 --- a/studybuilder-import/importers/run_import_standardcodelistterms1.py +++ b/studybuilder-import/importers/run_import_standardcodelistterms1.py @@ -99,7 +99,7 @@ async def handle_codelist_definitions(self, csvfile, session): "nci_preferred_name": row["preferred_term"] or "TBD", "definition": row["definition"] or "TBD", "extensible": extensible, - "ordinal": row["ordinal"], + "is_ordinal": row["ordinal"], "sponsor_preferred_name": row["new_codelist_name"], "template_parameter": template_parameter, "library_name": row["library"], diff --git a/studybuilder-import/importers/run_import_standardcodelistterms2.py b/studybuilder-import/importers/run_import_standardcodelistterms2.py index 5e4ce294..bb56d920 100644 --- a/studybuilder-import/importers/run_import_standardcodelistterms2.py +++ b/studybuilder-import/importers/run_import_standardcodelistterms2.py @@ -138,8 +138,8 @@ "MDR_MIGRATION_CATEGORY_FOR_SUBSTANCE_USE" ) MDR_DATA_SUPPLIER_TYPE = load_env("MDR_DATA_SUPPLIER_TYPE") -MDR_MIGRATION_LOCK_STUDY_MILESTONE = load_env("MDR_MIGRATION_LOCK_STUDY_MILESTONE") -MDR_MIGRATION_UNLOCK_STUDY_MILESTONE = load_env("MDR_MIGRATION_UNLOCK_STUDY_MILESTONE") +MDR_MIGRATION_REASON_FOR_LOCK = load_env("MDR_MIGRATION_REASON_FOR_LOCK") +MDR_MIGRATION_REASON_FOR_UNLOCK = load_env("MDR_MIGRATION_REASON_FOR_UNLOCK") # Import terms to standard codelists in sponsor library @@ -273,8 +273,8 @@ async def async_run(self): (MDR_MIGRATION_CATEGORY_FOR_EXPOSURE, "Category for Exposure"), (MDR_MIGRATION_CATEGORY_FOR_SUBSTANCE_USE, "Category for Substance Use"), (MDR_DATA_SUPPLIER_TYPE, "Data Supplier Type"), - (MDR_MIGRATION_LOCK_STUDY_MILESTONE, "Lock Study Milestone"), - (MDR_MIGRATION_UNLOCK_STUDY_MILESTONE, "Unlock Study Milestone"), + (MDR_MIGRATION_REASON_FOR_LOCK, "Reason For Lock"), + (MDR_MIGRATION_REASON_FOR_UNLOCK, "Reason For Unlock"), ] timeout = aiohttp.ClientTimeout(None) diff --git a/studybuilder/.eslintrc.js b/studybuilder/.eslintrc.js index 354ba838..6987858f 100644 --- a/studybuilder/.eslintrc.js +++ b/studybuilder/.eslintrc.js @@ -4,6 +4,9 @@ module.exports = { sourceType: 'module', ecmaVersion: 2022, }, + globals: { + globalThis: 'readonly', + }, env: { node: true, }, diff --git a/studybuilder/.github/agents/component-modernization.agent.md b/studybuilder/.github/agents/component-modernization.agent.md new file mode 100644 index 00000000..4f0d8f4f --- /dev/null +++ b/studybuilder/.github/agents/component-modernization.agent.md @@ -0,0 +1,78 @@ +--- +name: Component Modernization Agent +description: Modernizes StudyBuilder Vue components from Options API to Vue 3 Composition API, aligned with Vuetify 3 and project conventions. +--- + +# Component Modernization Agent + +## Role and Purpose + +You are a Vue 3 Component Modernization specialist for the StudyBuilder application. Your primary responsibility is to modernize Vue components from Options API to Composition API using ` + + +``` + +#### 2. Component Naming Conventions +- **File names**: PascalCase (e.g., `StudyActivityForm.vue`, `CheckboxField.vue`) +- **Component usage in templates**: PascalCase (enforced by ESLint) + ```vue + + ``` + +### Vuetify Components + +#### Common Vuetify Components and Props +```vue + + + Content + + + + + {{ $t('_global.save') }} + + + + + + + + + + + + + + + + + + + + + +``` + +#### Vuetify Styling Props +- **Density**: `density="compact"` or `density="comfortable"` +- **Rounded corners**: `rounded="lg"` (preferred) +- **Colors**: Use theme colors like `color="primary"`, `color="nnBaseBlue"`, `color="nnLightBlue200"` +- **Elevation**: `elevation="0"` for flat design +- **Variants**: `variant="outlined"`, `variant="flat"`, `variant="text"` + +### State Management with Pinia + +#### Store Definition Pattern +```javascript +import { defineStore } from 'pinia' +import apiModule from '@/api/module' + +export const useMyStore = defineStore('myStore', { + state: () => ({ + items: [], + currentItem: null, + isLoading: false, + }), + + getters: { + sortedItems: (state) => { + return [...state.items].sort((a, b) => a.name.localeCompare(b.name)) + }, + itemById: (state) => { + return (id) => state.items.find(item => item.id === id) + }, + }, + + actions: { + async fetchItems(params = {}) { + this.isLoading = true + try { + if (!params.page_size) params.page_size = 0 + if (!params.page_number) params.page_number = 1 + const resp = await apiModule.getItems(params) + this.items = resp.data.items + return resp + } finally { + this.isLoading = false + } + }, + + async createItem(data) { + const resp = await apiModule.createItem(data) + this.items.push(resp.data) + return resp + }, + + async updateItem(id, data) { + const resp = await apiModule.updateItem(id, data) + const index = this.items.findIndex(item => item.id === id) + if (index !== -1) { + this.items[index] = resp.data + } + return resp + }, + + async deleteItem(id) { + await apiModule.deleteItem(id) + this.items = this.items.filter(item => item.id !== id) + }, + }, +}) +``` + +#### Using Stores in Components +```vue + +``` + +### Composables + +#### Creating Reusable Composables +```javascript +// src/composables/myFeature.js +import { ref, computed } from 'vue' +import { escapeHTML } from '@/utils/sanitize' + +export function useMyFeature() { + const data = ref([]) + const isProcessing = ref(false) + + const processedData = computed(() => { + return data.value.map(item => ({ + ...item, + displayName: escapeHTML(item.name), + })) + }) + + const loadData = async () => { + isProcessing.value = true + try { + // Fetch data + } finally { + isProcessing.value = false + } + } + + return { + data, + isProcessing, + processedData, + loadData, + } +} +``` + +### Internationalization (i18n) + +#### Using Translations +```vue + + + +``` + +#### Translation Key Conventions +- Global keys: `_global.key_name` +- Component-specific: `ComponentName.key_name` +- Help text: `_help.ComponentName.key_name` + +### API Integration + +#### API Module Pattern +```javascript +// src/api/myModule.js +import { repository } from './repository' + +const resource = '/my-resource' + +export default { + getItems(params = {}) { + return repository.get(`${resource}`, { params }) + }, + + getItem(id) { + return repository.get(`${resource}/${id}`) + }, + + createItem(data) { + return repository.post(`${resource}`, data) + }, + + updateItem(id, data) { + return repository.patch(`${resource}/${id}`, data) + }, + + deleteItem(id) { + return repository.delete(`${resource}/${id}`) + }, +} +``` + +### Form Validation + +#### Using Form Rules +```vue + + + +``` + +### Styling Guidelines + +#### Use Vuetify Utility Classes +```vue + +``` + +#### Custom Colors (from theme) +- `primary`: Main brand color +- `nnBaseBlue`: Navy blue +- `nnLightBlue200`: Light blue +- `bg-dfltBackground`: Default background color +- `white-text`: White text + +### Best Practices + +#### 1. **Always sanitize user input for display** +```vue + + + +``` + +#### 2. **Use v-model for two-way binding** +```vue + + + +``` + +#### 3. **Handle loading and error states** +```vue + + + +``` + +#### 4. **Use data-cy attributes for testing** +```vue + +``` + +#### 5. **Provide meaningful accessibility attributes** +```vue + +``` + +### Code Formatting + +#### Prettier Configuration +- No semicolons (`;`) +- Single quotes (`'`) +- Trailing commas in ES5 compatible positions +- Auto-formatting on save + +#### Example +```javascript +const items = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, +] + +const result = items.map((item) => { + return item.name +}) +``` + +### Common Patterns + +#### 1. **Dialog/Form Pattern** +```vue + + + +``` + +#### 2. **List/Table with CRUD Operations** +```vue + + + +``` + +### Testing + +- Use `data-cy` attributes for Cypress E2E tests +- Component names should be descriptive and testable +- Form fields should have clear labels and validation + +## When Writing Code + +1. **Always use Composition API with ` +``` + +## Quality Checklist +- Test if the component compiles properly by running a format check `yarn format` +- Test if ESLint passes without errors `yarn lint` +- Test if build succeeds with `yarn build` + +## Reminder + +- Always write clean, maintainable, and well-documented code +- Follow the existing patterns in the codebase +- Use TypeScript-style JSDoc comments for better IDE support +- Prioritize user experience and accessibility +- Keep components focused and single-responsibility +- Use semantic HTML and ARIA attributes where appropriate + + diff --git a/studybuilder/.github/skills/component-modernization/SKILL.md b/studybuilder/.github/skills/component-modernization/SKILL.md new file mode 100644 index 00000000..a6c70631 --- /dev/null +++ b/studybuilder/.github/skills/component-modernization/SKILL.md @@ -0,0 +1,29 @@ +--- +name: vue-component-modernization +description: Modernize StudyBuilder Vue components from Options API to Vue 3 Composition API using diff --git a/studybuilder/src/components/library/ActivityInstanceOverview.vue b/studybuilder/src/components/library/ActivityInstanceOverview.vue index 51914c03..5d655a40 100644 --- a/studybuilder/src/components/library/ActivityInstanceOverview.vue +++ b/studybuilder/src/components/library/ActivityInstanceOverview.vue @@ -150,9 +150,22 @@ content-class="top-dialog" > + + + + @@ -164,15 +177,18 @@ import { ref, onMounted, watch, computed } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' +import { useFeatureFlagsStore } from '@/stores/feature-flags' import BaseActivityOverview from './BaseActivityOverview.vue' import ActivitySummary from './ActivitySummary.vue' import NNTable from '@/components/tools/NNTable.vue' +import ActivityInstanceForm from './ActivityInstanceForm.vue' import ActivitiesInstantiationsForm from './ActivitiesInstantiationsForm.vue' import ActivityItemsTable from './ActivityItemsTable.vue' import activities from '@/api/activities' const { t } = useI18n() const appStore = useAppStore() +const featureFlagsStore = useFeatureFlagsStore() const route = useRoute() const router = useRouter() @@ -397,6 +413,12 @@ const displayedGroupings = computed(() => { : convertActivityGroupingsToTableItems(activityGroupings.value) }) +const newWizardStepper = computed(() => { + return featureFlagsStore.getFeatureFlag( + 'new_activity_instance_wizard_stepper' + ) +}) + // Transform item for BaseActivityOverview function transformItem(item) { if (!item) return diff --git a/studybuilder/src/components/library/ActivityItemClassField.vue b/studybuilder/src/components/library/ActivityItemClassField.vue index f5a0837e..19ef9546 100644 --- a/studybuilder/src/components/library/ActivityItemClassField.vue +++ b/studybuilder/src/components/library/ActivityItemClassField.vue @@ -1,5 +1,5 @@ diff --git a/studybuilder/src/components/library/TermsSelectionForm.vue b/studybuilder/src/components/library/TermsSelectionForm.vue new file mode 100644 index 00000000..25233240 --- /dev/null +++ b/studybuilder/src/components/library/TermsSelectionForm.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/studybuilder/src/components/library/TestActivityItemClassField.vue b/studybuilder/src/components/library/TestActivityItemClassField.vue index 5009c15e..86fc934b 100644 --- a/studybuilder/src/components/library/TestActivityItemClassField.vue +++ b/studybuilder/src/components/library/TestActivityItemClassField.vue @@ -53,6 +53,7 @@ item-title="submission_value" class="ml-4 w-50" :rules="[formRules.required]" + :disabled="props.disabled" @updatecodelist="changeCodelist" />
diff --git a/studybuilder/src/components/library/UnitTable.vue b/studybuilder/src/components/library/UnitTable.vue index b16ef4c0..3adeb616 100644 --- a/studybuilder/src/components/library/UnitTable.vue +++ b/studybuilder/src/components/library/UnitTable.vue @@ -13,16 +13,16 @@ > diff --git a/studybuilder/src/components/library/crfs/CrfExtensionsManagementTable.vue b/studybuilder/src/components/library/crfs/CrfExtensionsManagementTable.vue index 91333754..fd2127ec 100644 --- a/studybuilder/src/components/library/crfs/CrfExtensionsManagementTable.vue +++ b/studybuilder/src/components/library/crfs/CrfExtensionsManagementTable.vue @@ -56,7 +56,6 @@ - diff --git a/studybuilder/src/components/library/crfs/CrfItemForm.vue b/studybuilder/src/components/library/crfs/CrfItemForm.vue index 25a374c4..71249f50 100644 --- a/studybuilder/src/components/library/crfs/CrfItemForm.vue +++ b/studybuilder/src/components/library/crfs/CrfItemForm.vue @@ -13,13 +13,13 @@ @save="submit" > + -