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}
예제 #4
0
    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
예제 #5
0
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
예제 #7
0
    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}
예제 #8
0
    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 []
예제 #10
0
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"
        )
예제 #11
0
    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",
        )
예제 #12
0
    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"}
예제 #13
0
    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
예제 #15
0
    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}
예제 #16
0
    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