def delete(self, dataset_uuid, study_id=None, user=None): """ Delete a dataset from a study given its unique identifier """ prop_id_to_name = get_property_map(key="id", value="name") prop_name_to_id = reverse_map(prop_id_to_name) # Used for helper route using only dataset_uuid if study_id is None: study_id = find_study_id_from_lvl1_uuid("dataset", dataset_uuid, prop_name_to_id) if study_id is None: raise Exception( f"Dataset not found in any study (uuid = {dataset_uuid})") # 1. Get study data study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) # 2. Delete specific dataset datasets_entry = study_converter.get_entry_by_name("datasets") datasets_entry.value.delete_nested_entry("uuid", dataset_uuid) if len(datasets_entry.value.value) == 0: study_converter.remove_entries(prop_names=["datasets"]) # 3. Update study state, data and ulpoad on DB message = f"Deleted dataset" update_study(study, study_converter, api.payload, message, user) return {"message": message}
def get(self, dataset_uuid, study_id=None, user=None): """ Fetch a specific dataset for a given study """ args = self._get_parser.parse_args() prop_id_to_name = get_property_map(key="id", value="name") prop_name_to_id = reverse_map(prop_id_to_name) # Used for helper route using only dataset_uuid if study_id is None: study_id = find_study_id_from_lvl1_uuid("dataset", dataset_uuid, prop_name_to_id) if study_id is None: raise Exception( f"Dataset not found in any study (uuid = {dataset_uuid})") study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) # The converter is used for its get_entry_by_name() method study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) datasets_entry = study_converter.get_entry_by_name("datasets") dataset_nested_entry = datasets_entry.value.find_nested_entry( "uuid", dataset_uuid)[0] # The "dataset_nested_entry" entry is a NestedEntry (return list of dict) if args["entry_format"] == "api": return dataset_nested_entry.get_api_format() elif args["entry_format"] == "form": return dataset_nested_entry.get_form_format()
def put(self, dataset_uuid, study_id=None, user=None): """ Update a dataset for a given study """ prop_id_to_name = get_property_map(key="id", value="name") prop_name_to_id = reverse_map(prop_id_to_name) # Used for helper route using only dataset_uuid if study_id is None: study_id = find_study_id_from_lvl1_uuid("dataset", dataset_uuid, prop_name_to_id) if study_id is None: raise Exception( f"Dataset not found in any study (uuid = {dataset_uuid})") payload = api.payload # 1. Split payload form_name = payload["form_name"] entries = payload["entries"] entry_format = payload.get("entry_format", "api") # 2. Get study data study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) # 3. Get current dataset data datasets_entry = study_converter.get_entry_by_name("datasets") dataset_nested_entry = datasets_entry.value.find_nested_entry( "uuid", dataset_uuid)[0] dataset_converter = FormatConverter(mapper=prop_id_to_name) dataset_converter.entries = dataset_nested_entry.value # 4. Get new dataset data and get entries to remove new_dataset_converter, entries_to_remove = get_entity_converter( entries, entry_format, prop_id_to_name, prop_name_to_id) # 5. Update current dataset by adding, updating and deleting entries # Nested entries not present in the original form are ignored (example: dataset["samples"]) # won't be deleted if not present in the new data), it needs to be None or "" to be deleted dataset_converter.add_or_update_entries(new_dataset_converter.entries) dataset_converter.remove_entries(entries=entries_to_remove) dataset_nested_entry.value = dataset_converter.entries # 6. Validate dataset data against form validate_form_format_against_form(form_name, dataset_converter.get_form_format()) # 7. Update study state, data and ulpoad on DB message = "Updated dataset" update_study(study, study_converter, payload, message, user) return {"message": message}
def get(self, user=None): """ Fetch a list with all entries """ # Convert query parameters args = self._get_parser.parse_args() include_deprecate = args["deprecated"] res = Study.objects() if not include_deprecate: res = res.filter(meta_information__deprecated=False) study_ids = [ u.strip() for u in re.split(r",|;", args["study_ids"]) if u.strip().lower() not in ["", "0", "none", "false"] ] if len(study_ids) > 0: res = res.filter(id__in=study_ids) if res.count() < len(study_ids): found_ids = [str(s.id) for s in res.only("id")] return { "message": f"Some study ids were not found", "missing_study_ids": list(set(study_ids) - set(found_ids)), }, 404 # Limits and Skipping applied after main filters res = res.all().skip(args["skip"]) # Issue with limit(0) that returns 0 items instead of all of them if args["limit"] != 0: res = res.limit(args["limit"]) if args["properties_id_only"] or args["entry_format"] == "form": marchal_model = study_model_prop_id else: marchal_model = study_model # Marshal studies study_json_list = marshal(list(res.select_related()), marchal_model) if args["entry_format"] == "api": return study_json_list elif args["entry_format"] == "form": prop_map = get_property_map(key="id", value="name") for study_json in study_json_list: study_converter = FormatConverter(prop_map).add_api_format( study_json["entries"] ) study_json["entries"] = study_converter.get_form_format() return study_json_list
def validate_against_form(form_cls, form_name, entries): prop_map = get_property_map(key="id", value="name") form_data_json = FormatConverter(prop_map).add_api_format(entries).get_form_format() # 3. Validate data against form form_instance = form_cls() form_instance.process(data=form_data_json) if not form_instance.validate(): raise RequestBodyException( f"Passed data did not validate with the form {form_name}: {form_instance.errors}" ) return form_data_json
def post(self, study_id, user=None): """ Add a new dataset for a given study """ payload = api.payload prop_id_to_name = get_property_map(key="id", value="name") prop_name_to_id = reverse_map(prop_id_to_name) # 1. Split payload form_name = payload["form_name"] entries = payload["entries"] entry_format = payload.get("entry_format", "api") # 2. Get study data study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) # 3. Create dataset entry and append it to "datasets" # Format and clean entity dataset_converter, _ = get_entity_converter(entries, entry_format, prop_id_to_name, prop_name_to_id) # Generate UUID dataset_converter, dataset_uuid = add_uuid_entry_if_missing( dataset_converter, prop_name_to_id) study_converter = add_entity_to_study_nested_list( study_converter=study_converter, entity_converter=dataset_converter, prop_name_to_id=prop_name_to_id, study_list_prop="datasets", ) # 4. Validate dataset data against form validate_form_format_against_form(form_name, dataset_converter.get_form_format()) # 5. Update study state, data and ulpoad on DB message = "Added dataset" update_study(study, study_converter, payload, message, user) return {"message": message, "uuid": dataset_uuid}, 201
def delete(self, study_id, user=None): """ Delete all samples from a study given its unique identifier """ prop_id_to_name = get_property_map(key="id", value="name") # 1. Get study data study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) # 2. Delete samples study_converter.remove_entries(prop_names=["samples"]) # 3. Update study state, data and ulpoad on DB message = "Deleted samples" update_study(study, study_converter, api.payload, message, user) return {"message": message}
def get(self, id, user=None): """Fetch an entry given its unique identifier""" args = self._get_parser.parse_args() if args["properties_id_only"] or args["entry_format"] == "form": marchal_model = study_model_prop_id else: marchal_model = study_model study_json = marshal(Study.objects(id=id).get(), marchal_model) if args["entry_format"] == "api": return study_json elif args["entry_format"] == "form": prop_map = get_property_map(key="id", value="name") study_converter = FormatConverter(prop_map).add_api_format( study_json["entries"] ) study_json["entries"] = study_converter.get_form_format() return study_json
def get(self, study_id, user=None): """ Fetch a list of all datasets for a given study """ args = self._get_parser.parse_args() study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) prop_map = get_property_map(key="id", value="name") # The converter is used for its get_entry_by_name() method study_converter = FormatConverter(mapper=prop_map) study_converter.add_api_format(study_json["entries"]) datasets_entry = study_converter.get_entry_by_name("datasets") if datasets_entry is not None: # The "datasets" entry is a NestedListEntry (return list of list) if args["entry_format"] == "api": return datasets_entry.get_api_format() elif args["entry_format"] == "form": return datasets_entry.get_form_format() else: return []
def download_denorm_file(request_args, data, header_prefix_to_suffix, file_name): """ Download samples in a denormalized file Arguments - request = flask request - data in denormalized 2 format (see NormConverter.get_denorm_data_2_from_nested) - header_prefix_to_suffix = dict to replace headers with nice suffixes - file_name = exported file name (without extension) Query arguments - header_sep = seaparator used to join header across nesting: samples__treatment__treatment_id Default: __ - format = file formats supported for the file export (xlsx, tsv or csv) Default: xlsx - prop_to_ignore = comma separated list of property names to ignore (default to "uuid") Default: uuid - use_cv_labels = true / false ==> If true, will replace CV item names by their labels Default: true - prettify_headers = true / false ==> If true, will replace property name by labels and header prefixes by shorter suffixes Default: true """ try: # 1. Parse query parameters header_sep = request_args["header_sep"].strip() file_format = request_args["format"].strip().lower() use_cv_labels = request_args["use_cv_labels"] prettify_headers = request_args["prettify_headers"] prop_to_ignore = [ p.strip().lower() for p in request_args["prop_to_ignore"].split(",") if p.strip().lower() != "" ] # 2. Ignore certain properties for header in list(data.keys()): property_name = header.split(header_sep)[-1] if property_name in prop_to_ignore: data.pop(header) # 3. Convert CV item names to labels if use_cv_labels: prop_name_to_data_type = get_property_map( key="name", value="value_type", mask="name, value_type{data_type, controlled_vocabulary{name}}", ) cv_items_name_to_label_map = get_cv_items_map(key="name", value="label") for header, data_list in data.items(): property_name = header.split(header_sep)[-1] prop_value_type = prop_name_to_data_type.get(property_name, None) if ( prop_value_type is not None and prop_value_type["data_type"] == "ctrl_voc" ): cv_name = prop_value_type["controlled_vocabulary"]["name"] cv_items_map = cv_items_name_to_label_map[cv_name] new_data_list = [] for value in data_list: if type(value) == list: new_data_list.append( [cv_items_map.get(v, v) for v in value] ) else: new_data_list.append(cv_items_map.get(value, value)) data[header] = new_data_list # 4. Stringify simple lists for header, data_list in data.items(): if type(data_list[0]) == list: data[header] = [", ".join(map(str, v)) for v in data_list] # 5. Update headers if prettify_headers: prop_name_to_label = get_property_map(key="name", value="label") old_data = data data = OrderedDict() for header, data_list in old_data.items(): split_header = header.rsplit(header_sep, 1) prefix = split_header[0] if len(split_header) > 1 else "" prop_name = ( split_header[1] if len(split_header) > 1 else split_header[0] ) if prop_name in prop_name_to_label: prop_label = prop_name_to_label[prop_name] else: prop_label = prop_name if prefix in header_prefix_to_suffix: suffix = header_prefix_to_suffix[prefix] new_header = f"{prop_label} ({suffix})" else: new_header = f"{prop_label}" data[new_header] = data_list # 6. Write file f = tempfile.NamedTemporaryFile(mode="w", delete=False) write_file_from_denorm_data_2(f, data, file_format) f.close() @after_this_request def cleanup(response): os.unlink(f.name) return response response = send_file( f.name, as_attachment=True, attachment_filename=f"{file_name}.{file_format}" ) response.headers.extend({"Cache-Control": "no-cache"}) return response except Exception as e: from traceback import print_exc print_exc() return Response( json.dumps({"error": str(e)}), status=500, mimetype="application/json" )
def get(self, dataset_uuid, study_id=None, user=None): """Download readouts in a denormalized file""" args = self._get_parser.parse_args() prettify_headers = args["prettify_headers"] header_sep = "__" if prettify_headers else args["header_sep"].strip() study_endpoint = urljoin(app.config["URL"], os.environ["API_EP_STUDY"]) header_prefix_to_suffix = { "": "STU", "datasets": "DAT", "datasets__readouts": "RDT", "datasets__readouts__samples": "SAM", "datasets__readouts__samples__individual__treatment": "TRE > IND", "datasets__readouts__samples__treatment": "TRE > SAM", "datasets__readouts__samples__individual": "IND", } # 1. Get the study, dataset and readouts data if study_id is None: prop_name_to_id = get_property_map(key="name", value="id") study_id = find_study_id_from_lvl1_uuid( "dataset", dataset_uuid, prop_name_to_id ) study_url = f"{study_endpoint}/id/{study_id}?entry_format=form" study = get_json(study_url, headers=request.headers)["entries"] for d in study["datasets"]: if d["uuid"] == dataset_uuid: dataset = d break else: raise Exception(f"Dataset '{dataset_uuid}' not found in study '{study_id}'") if not "readouts" in dataset: raise Exception(f"Dataset '{dataset_uuid}' doesn't have exeperiments data") if not "samples" in study: raise Exception(f"The given study '{study_id}' doesn't have samples data") # 2. Replace sample UUIDs in readouts by nested sample objects sam_uuid_to_obj = map_key_value_from_dict_list( study["samples"], key="uuid", value=None ) try: for readout in dataset["readouts"]: readout["samples"] = [ sam_uuid_to_obj[uuid] for uuid in readout["samples"] ] except: message = "Readouts sample UUIDs did not match the samples of the study," message += " please update the readouts if the samples have been changed" raise Exception(message) # 3. Removing data we don't want in the file # 3.1. Relevant samples are in dataset > readouts del study["samples"] # 3.2. Removing processing event data to avoid too much denormalization and duplication of lines if "process_events" in dataset: dataset["process_events"] = len(dataset["process_events"]) # 3.3. Only interested in one dataset study["datasets"] = dataset # 4. Expand JSON string properties dataset = expand_json_strings(dataset) study = expand_json_strings(study) # 5. Convert to flat format (denormalized) # 5.1. Readouts converter = NormConverter(nested_data=study["datasets"]["readouts"]) data_flat = converter.get_denorm_data_2_from_nested( vars_to_denorm=["samples"], use_parent_key=True, sep=header_sep, initial_parent_key="datasets__readouts", missing_value="", ) # 5.2. Add dataset data nb_lines = len(list(data_flat.values())[0]) for dataset_prop, value in dataset.items(): if not dataset_prop in ["readouts"]: data_flat[f"datasets__{dataset_prop}"] = [value] * nb_lines # 5.3. Add study data for study_prop, value in study.items(): if not study_prop in ["datasets"]: data_flat[study_prop] = [value] * nb_lines return download_denorm_file( request_args=args, data=data_flat, header_prefix_to_suffix=header_prefix_to_suffix, file_name="readouts", )
def put(self, id, user=None): """ Update an entry given its unique identifier """ payload = api.payload # 1. Split payload study_id = id form_name = payload["form_name"] entries = payload["entries"] entry_format = payload.get("entry_format", "api") study = Study.objects(id=study_id).first() prop_map = get_property_map(key="name", value="id") # 1. Extract form name and create form from FormManager form_cls = app.form_manager.get_form_by_name(form_name=form_name) # 2. Make sure to have both API and form format if entry_format == "api": entries = { "api_format": entries, "form_format": validate_against_form(form_cls, form_name, entries), } else: validate_form_format_against_form(form_name, entries) entries = { "api_format": FormatConverter(prop_map) .add_form_format(entries) .get_api_format(), "form_format": entries, } # 3. Check unicity of pseudo alternate pk in entries # check_alternate_pk_unicity(entries=entries["form_format"], pseudo_apks=["study_id"], prop_map=prop_map) # 4. Determine current state and evaluate next state state_name = str(study.meta_information.state) if state_name == "rna_sequencing_biokit": state_name = "BiokitUploadState" app.study_state_machine.load_state(name=state_name) app.study_state_machine.change_state(**entries["form_format"]) new_state = app.study_state_machine.current_state # 5. Create and append meta information to the study meta_info = MetaInformation( state=state_name, change_log=study.meta_information.change_log ) log = ChangeLog( action="Updated study", user_id=user.id if user else None, timestamp=datetime.now(), manual_user=payload.get("manual_meta_information", {}).get("user", None), ) meta_info.state = str(new_state) meta_info.add_log(log) study_data = { "entries": entries["api_format"], "meta_information": meta_info.to_json(), } # 6. Update data in database study.update(**study_data) # Index study on ES index_study_if_es(study, entries["form_format"], "update") return {"message": f"Update study"}
def post(self, user=None): """ Add a new entry """ payload = api.payload # 1. Split payload form_name = payload["form_name"] initial_state = payload["initial_state"] entries = payload["entries"] entry_format = payload.get("entry_format", "api") form_cls = app.form_manager.get_form_by_name(form_name=form_name) if initial_state == "rna_sequencing_biokit": initial_state = "BiokitUploadState" app.study_state_machine.load_state(name=initial_state) prop_map = get_property_map(key="name", value="id") # 2. Make sure to have both API and form format if entry_format == "api": try: if len(entries) != len({prop["property"] for prop in entries}): raise IdenticalPropertyException( "The entries cannot have several identical property values." ) except TypeError as e: raise RequestBodyException("Entries has wrong format.") from e entries = { "api_format": entries, "form_format": validate_against_form(form_cls, form_name, entries), } else: validate_form_format_against_form(form_name, entries) entries = { "api_format": FormatConverter(prop_map) .add_form_format(entries) .get_api_format(), "form_format": entries, } # 3. Check unicity of pseudo alternate pk in entries check_alternate_pk_unicity( entries=entries["form_format"], pseudo_apks=["study_id"], prop_map=prop_map ) # 4. Evaluate new state of study by passing form data app.study_state_machine.create_study(**entries["form_format"]) state = app.study_state_machine.current_state meta_info = MetaInformation(state=str(state)) log = ChangeLog( user_id=user.id if user else None, action="Created study", timestamp=datetime.now(), manual_user=payload.get("manual_meta_information", {}).get("user", None), ) meta_info.add_log(log) study_data = { "entries": entries["api_format"], "meta_information": meta_info.to_json(), } # 5. Insert data into database study = Study(**study_data) study.save() # Index study on ES index_study_if_es(study, entries["form_format"], "add") return {"message": f"Study added", "id": str(study.id)}, 201
def post(self, dataset_uuid, study_id=None, user=None): """ Add a new processing event for a given dataset """ prop_id_to_name = get_property_map(key="id", value="name") prop_name_to_id = reverse_map(prop_id_to_name) # Used for helper route using only dataset_uuid if study_id is None: study_id = find_study_id_from_lvl1_uuid("dataset", dataset_uuid, prop_name_to_id) if study_id is None: raise Exception( f"Dataset not found in any study (uuid = {dataset_uuid})") payload = api.payload # 1. Split payload form_name = payload["form_name"] entries = payload["entries"] entry_format = payload.get("entry_format", "api") # 2. Get study and dataset data study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) datasets_entry = study_converter.get_entry_by_name("datasets") dataset_nested_entry, dataset_position = datasets_entry.value.find_nested_entry( "uuid", dataset_uuid) # 3. Format and clean processing event data pe_converter, _ = get_entity_converter(entries, entry_format, prop_id_to_name, prop_name_to_id) # 4. Generate UUID pe_converter, pe_uuid = add_uuid_entry_if_missing(pe_converter, prop_name_to_id, replace=False) pe_nested_entry = NestedEntry(pe_converter) pe_nested_entry.value = pe_converter.entries # 5. Check if "process_events"" entry already exist study, creates it if it doesn't pes_entry = dataset_nested_entry.get_entry_by_name("process_events") if pes_entry is not None: pes_entry.value.value.append(pe_nested_entry) else: pes_entry = Entry( FormatConverter(prop_name_to_id)).add_form_format( "process_events", [pe_nested_entry.get_form_format()]) dataset_nested_entry.value.append(pes_entry) datasets_entry.value.value[dataset_position] = dataset_nested_entry # 6. Validate processing data against form validate_form_format_against_form(form_name, pe_converter.get_form_format()) # 7. Update study state, data and ulpoad on DB message = "Added processing event" update_study(study, study_converter, payload, message, user) return {"message": message, "uuid": pe_uuid}, 201
def put(self, sample_uuid, study_id=None, user=None): """ Update a sample for a given study """ prop_id_to_name = get_property_map(key="id", value="name") prop_name_to_id = reverse_map(prop_id_to_name) # Used for helper route using only sample_uuid if study_id is None: study_id = find_study_id_from_lvl1_uuid("sample", sample_uuid, prop_name_to_id) if study_id is None: raise Exception( f"Sample not found in any study (uuid = {sample_uuid})") payload = api.payload # 1. Split payload validate_dict = payload.get("validate", None) form_names = payload.get("form_names", None) entries = payload["entries"] entry_format = payload.get("entry_format", "api") # 2. Get forms for validation forms = {} for key, validate in validate_dict.items(): if validate: forms[key] = app.form_manager.get_form_by_name( form_name=form_names[key]) # 3. Get study data study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) # 3. Get current sample data samples_entry = study_converter.get_entry_by_name("samples") sample_nested_entry = samples_entry.value.find_nested_entry( "uuid", sample_uuid)[0] sample_converter = FormatConverter(mapper=prop_id_to_name) sample_converter.entries = sample_nested_entry.value # 4. Unify UUIDs with existing entities (including nested ones) # Format and clean entity new_sample_converter, _ = get_entity_converter(entries, entry_format, prop_id_to_name, prop_name_to_id) new_sample_form_format = new_sample_converter.get_form_format() [new_sample_form_format] = unify_sample_entities_uuids( existing_samples=study_converter.get_form_format().get( "samples", []), new_samples=[new_sample_form_format], ) # 5. Clean new data and get entries to remove # Format and clean entity new_sample_converter, entries_to_remove = get_entity_converter( entries=new_sample_form_format, entry_format="form", prop_id_to_name=None, prop_name_to_id=prop_name_to_id, ) # 6. Update current sample by adding, updating and deleting entries # Nested entries not present in the original form are ignored # won't be deleted if not present in the new data), it needs to be None or "" to be deleted sample_converter.add_or_update_entries(new_sample_converter.entries) sample_converter.remove_entries(entries=entries_to_remove) sample_nested_entry.value = sample_converter.entries # 7. Validate data against form validate_sample_against_form(sample_converter.get_form_format(), validate_dict, forms) # 8. Update study state, data and ulpoad on DB message = "Updated sample" update_study(study, study_converter, payload, message, user) return {"message": message}
def post(self, study_id, user=None): """ Add a new sample for a given study """ payload = api.payload prop_id_to_name = get_property_map(key="id", value="name") prop_name_to_id = reverse_map(prop_id_to_name) # 1. Split payload validate_dict = payload.get("validate", None) form_names = payload.get("form_names", None) entries = payload["entries"] entry_format = payload.get("entry_format", "api") # 2. Get forms for validation forms = {} for key, validate in validate_dict.items(): if validate: forms[key] = app.form_manager.get_form_by_name( form_name=form_names[key]) # 3. Get study data study = Study.objects().get(id=study_id) study_json = marshal(study, study_model) study_converter = FormatConverter(mapper=prop_id_to_name) study_converter.add_api_format(study_json["entries"]) # 4. Unify UUIDs with existing entities (including nested ones) # Format and clean entity sample_converter, _ = get_entity_converter(entries, entry_format, prop_id_to_name, prop_name_to_id) new_sample_form_format = sample_converter.get_form_format() [new_sample_form_format] = unify_sample_entities_uuids( existing_samples=study_converter.get_form_format().get( "samples", []), new_samples=[new_sample_form_format], ) # 5. Append new samples to "samples" in study # Format and clean entity sample_converter, _ = get_entity_converter( entries=new_sample_form_format, entry_format="form", prop_id_to_name=None, prop_name_to_id=prop_name_to_id, ) # Generate UUID (redundant, UUIDs already generated by unify_sample_entities_uuids) sample_converter, sample_uuid = add_uuid_entry_if_missing( sample_converter, prop_name_to_id) study_converter = add_entity_to_study_nested_list( study_converter=study_converter, entity_converter=sample_converter, prop_name_to_id=prop_name_to_id, study_list_prop="samples", ) # 6. Validate data against form validate_sample_against_form(sample_converter.get_form_format(), validate_dict, forms) # 7. Update study state, data and ulpoad on DB message = "Added sample" update_study(study, study_converter, payload, message, user) return {"message": message, "uuid": sample_uuid}, 201