From 6e7e6be78824f9a388b71504c5fb2ef246f93c3b Mon Sep 17 00:00:00 2001 From: Daniel Rabstejnek Date: Thu, 12 Oct 2023 09:58:29 -0400 Subject: [PATCH 01/11] Epi export rewrite (#911) * preliminary epi rewrite * changes * changes * changes * fix * added back something accidentally deleted * moved code * fix test * remove old stuff * cleanup: * use vectorized timestamp conversion * minor formatting --------- Co-authored-by: Andy Shapiro --- hawc/apps/common/exports.py | 23 +- hawc/apps/epi/exports.py | 866 +++++++++++++----- hawc/apps/epi/models.py | 352 ------- hawc/apps/study/exports.py | 17 +- tests/data/api/api-dp-data-epi.json | 12 +- tests/data/api/api-epi-assessment-export.json | 84 +- 6 files changed, 690 insertions(+), 664 deletions(-) diff --git a/hawc/apps/common/exports.py b/hawc/apps/common/exports.py index 6693a06ad2..35da26388d 100644 --- a/hawc/apps/common/exports.py +++ b/hawc/apps/common/exports.py @@ -1,5 +1,6 @@ import pandas as pd from django.db.models import QuerySet +from django.utils import timezone from .helper import FlatExport @@ -14,18 +15,10 @@ def __init__( include: tuple[str, ...] | None = None, exclude: tuple[str, ...] | None = None, ): - """Instantiate an exporter instance for a given django model. - - Args: - key_prefix (str, optional): The model name to prepend to data frame columns. - query_prefix (str, optional): The model prefix in the ORM. - include (tuple | None, optional): If included, only these items are added. - exclude (tuple | None, optional): If specified, items are removed from base. - """ self.key_prefix = key_prefix + "-" if key_prefix else key_prefix self.query_prefix = query_prefix + "__" if query_prefix else query_prefix - self.include = (key_prefix + field for field in include) if include else tuple() - self.exclude = (key_prefix + field for field in exclude) if exclude else tuple() + self.include = tuple(self.key_prefix + field for field in include) if include else tuple() + self.exclude = tuple(self.key_prefix + field for field in exclude) if exclude else tuple() @property def value_map(self) -> dict: @@ -153,6 +146,15 @@ def prepare_df(self, df: pd.DataFrame) -> pd.DataFrame: """ return df + def format_time(self, df: pd.DataFrame) -> pd.DataFrame: + if df.shape[0] == 0: + return df + tz = timezone.get_default_timezone() + for key in [self.get_column_name("created"), self.get_column_name("last_updated")]: + if key in df.columns: + df.loc[:, key] = df[key].dt.tz_convert(tz).dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z") + return df + def get_df(self, qs: QuerySet) -> pd.DataFrame: """Get dataframe export from queryset. @@ -211,7 +213,6 @@ def get_df(self, qs: QuerySet) -> pd.DataFrame: @classmethod def flat_export(cls, qs: QuerySet, filename: str) -> FlatExport: """Return an instance of a FlatExport. - Args: qs (QuerySet): the initial QuerySet filename (str): the filename for the export diff --git a/hawc/apps/epi/exports.py b/hawc/apps/epi/exports.py index befea2868c..b28e2d096a 100644 --- a/hawc/apps/epi/exports.py +++ b/hawc/apps/epi/exports.py @@ -1,266 +1,632 @@ +import math + +import pandas as pd +from django.db.models import Case, Q, When + +from ..common.exports import Exporter, ModelExport from ..common.helper import FlatFileExporter +from ..common.models import sql_display, sql_format, str_m2m from ..materialized.models import FinalRiskOfBiasScore -from ..study.models import Study -from . import models +from ..study.exports import StudyExport +from . import constants, models -class OutcomeComplete(FlatFileExporter): - def _get_header_row(self): - header = [] - header.extend(Study.flat_complete_header_row()) - header.extend(models.StudyPopulation.flat_complete_header_row()) - header.extend(models.Outcome.flat_complete_header_row()) - header.extend(models.Exposure.flat_complete_header_row()) - header.extend(models.ComparisonSet.flat_complete_header_row()) - header.extend(models.Result.flat_complete_header_row()) - header.extend(models.Group.flat_complete_header_row()) - header.extend(models.GroupResult.flat_complete_header_row()) - return header - - def _get_data_rows(self): - rows = [] - identifiers_df = Study.identifiers_df(self.queryset, "study_population__study_id") - for obj in self.queryset: - ser = obj.get_json(json_encode=False) - row = [] - row.extend( - Study.flat_complete_data_row(ser["study_population"]["study"], identifiers_df) +def percent_control(n_1, mu_1, sd_1, n_2, mu_2, sd_2): + mean = low = high = None + + if mu_1 and mu_2 and mu_1 != 0: + mean = (mu_2 - mu_1) / mu_1 * 100.0 + if sd_1 and sd_2 and n_1 and n_2: + sd = math.sqrt( + pow(mu_1, -2) + * ((pow(sd_2, 2) / n_2) + (pow(mu_2, 2) * pow(sd_1, 2)) / (n_1 * pow(mu_1, 2))) ) - row.extend(models.StudyPopulation.flat_complete_data_row(ser["study_population"])) - row.extend(models.Outcome.flat_complete_data_row(ser)) - for res in ser["results"]: - row_copy = list(row) - row_copy.extend( - models.Exposure.flat_complete_data_row(res["comparison_set"]["exposure"]) - ) - row_copy.extend(models.ComparisonSet.flat_complete_data_row(res["comparison_set"])) - row_copy.extend(models.Result.flat_complete_data_row(res)) - for rg in res["results"]: - row_copy2 = list(row_copy) - row_copy2.extend(models.Group.flat_complete_data_row(rg["group"])) - row_copy2.extend(models.GroupResult.flat_complete_data_row(rg)) - rows.append(row_copy2) - return rows + ci = (1.96 * sd) * 100 + rng = sorted([mean - ci, mean + ci]) + low = rng[0] + high = rng[1] + + return mean, low, high + + +class StudyPopulationExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "url": "url", + "name": "name", + "design": "design_display", + "age_profile": "age_profile", + "source": "source", + "countries": "countries__name", + "region": "region", + "state": "state", + "eligible_n": "eligible_n", + "invited_n": "invited_n", + "participant_n": "participant_n", + "inclusion_criteria": "inclusion_criteria", + "exclusion_criteria": "exclusion_criteria", + "confounding_criteria": "confounding_criteria", + "comments": "comments", + "created": "created", + "last_updated": "last_updated", + } + + def get_annotation_map(self, query_prefix): + return { + "url": sql_format("/epi/study-population/{}/", query_prefix + "id"), # hardcoded URL + "design_display": sql_display(query_prefix + "design", constants.Design), + "countries__name": str_m2m(query_prefix + "countries__name"), + "inclusion_criteria": str_m2m( + query_prefix + "spcriteria__criteria__description", + filter=Q(**{query_prefix + "spcriteria__criteria_type": constants.CriteriaType.I}), + ), + "exclusion_criteria": str_m2m( + query_prefix + "spcriteria__criteria__description", + filter=Q(**{query_prefix + "spcriteria__criteria_type": constants.CriteriaType.E}), + ), + "confounding_criteria": str_m2m( + query_prefix + "spcriteria__criteria__description", + filter=Q(**{query_prefix + "spcriteria__criteria_type": constants.CriteriaType.C}), + ), + } + + def prepare_df(self, df): + return self.format_time(df) + + +class OutcomeExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "url": "url", + "name": "name", + "effects": "effects__name", + "system": "system", + "effect": "effect", + "effect_subtype": "effect_subtype", + "diagnostic": "diagnostic_display", + "diagnostic_description": "diagnostic_description", + "age_of_measurement": "age_of_measurement", + "outcome_n": "outcome_n", + "summary": "summary", + "created": "created", + "last_updated": "last_updated", + } + + def get_annotation_map(self, query_prefix): + return { + "url": sql_format("/epi/outcome/{}/", query_prefix + "id"), # hardcoded URL + "effects__name": str_m2m(query_prefix + "effects__name"), + "diagnostic_display": sql_display(query_prefix + "diagnostic", constants.Diagnostic), + } + + def prepare_df(self, df): + return self.format_time(df) + + +class ExposureExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "url": "url", + "name": "name", + "inhalation": "inhalation", + "dermal": "dermal", + "oral": "oral", + "in_utero": "in_utero", + "iv": "iv", + "unknown_route": "unknown_route", + "measured": "measured", + "metric": "metric", + "metric_units_id": "metric_units__id", + "metric_units_name": "metric_units__name", + "metric_description": "metric_description", + "analytical_method": "analytical_method", + "sampling_period": "sampling_period", + "age_of_exposure": "age_of_exposure", + "duration": "duration", + "n": "n", + "exposure_distribution": "exposure_distribution", + "description": "description", + "created": "created", + "last_updated": "last_updated", + } + + def get_annotation_map(self, query_prefix): + return { + "url": sql_format("/epi/exposure/{}/", query_prefix + "id"), # hardcoded URL + } + + def prepare_df(self, df): + return self.format_time(df) + + +class ComparisonSetExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "url": "url", + "name": "name", + "description": "description", + "created": "created", + "last_updated": "last_updated", + } + + def get_annotation_map(self, query_prefix): + return { + "url": sql_format("/epi/comparison-set/{}/", query_prefix + "id"), # hardcoded URL + } + + def prepare_df(self, df): + return self.format_time(df) + + +class ResultMetricExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "name": "metric", + "abbreviation": "abbreviation", + } + + +class ResultExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "name": "name", + "metric_description": "metric_description", + "metric_units": "metric_units", + "data_location": "data_location", + "population_description": "population_description", + "dose_response": "dose_response_display", + "dose_response_details": "dose_response_details", + "prevalence_incidence": "prevalence_incidence", + "statistical_power": "statistical_power_display", + "statistical_power_details": "statistical_power_details", + "statistical_test_results": "statistical_test_results", + "trend_test": "trend_test", + "adjustment_factors": "adjustment_factors", + "adjustment_factors_considered": "adjustment_factors_considered", + "estimate_type": "estimate_type_display", + "variance_type": "variance_type_display", + "ci_units": "ci_units", + "comments": "comments", + "created": "created", + "last_updated": "last_updated", + "tags": "tags", + } + + def get_annotation_map(self, query_prefix): + return { + "dose_response_display": sql_display( + query_prefix + "dose_response", constants.DoseResponse + ), + "adjustment_factors": str_m2m( + query_prefix + "resfactors__adjustment_factor__description", + filter=Q(**{query_prefix + "resfactors__included_in_final_model": True}), + ), + "adjustment_factors_considered": str_m2m( + query_prefix + "resfactors__adjustment_factor__description", + filter=Q(**{query_prefix + "resfactors__included_in_final_model": False}), + ), + "statistical_power_display": sql_display( + query_prefix + "statistical_power", constants.StatisticalPower + ), + "estimate_type_display": sql_display( + query_prefix + "estimate_type", constants.EstimateType + ), + "variance_type_display": sql_display( + query_prefix + "variance_type", constants.VarianceType + ), + "tags": str_m2m(query_prefix + "resulttags__name"), + } + + def prepare_df(self, df): + return self.format_time(df) + + +class GroupExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "group_id": "group_id", + "name": "name", + "numeric": "numeric", + "comparative_name": "comparative_name", + "sex": "sex_display", + "ethnicities": "ethnicities", + "eligible_n": "eligible_n", + "invited_n": "invited_n", + "participant_n": "participant_n", + "isControl": "isControl", + "comments": "comments", + "created": "created", + "last_updated": "last_updated", + } + + def get_annotation_map(self, query_prefix): + return { + "sex_display": sql_display(query_prefix + "sex", constants.Sex), + "ethnicities": str_m2m(query_prefix + "ethnicities__name"), + } + + def prepare_df(self, df): + return self.format_time(df) + + +class GroupResultExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "n": "n", + "estimate": "estimate", + "variance": "variance", + "lower_ci": "lower_ci", + "upper_ci": "upper_ci", + "lower_range": "lower_range", + "upper_range": "upper_range", + "lower_bound_interval": "lower_bound_interval", + "upper_bound_interval": "upper_bound_interval", + "p_value_qualifier": "p_value_qualifier_display", + "p_value": "p_value", + "is_main_finding": "is_main_finding", + "main_finding_support": "main_finding_support_display", + "created": "created", + "last_updated": "last_updated", + } + + def get_annotation_map(self, query_prefix): + return { + "lower_bound_interval": Case( + When(**{query_prefix + "lower_ci": None}, then=query_prefix + "lower_range"), + default=query_prefix + "lower_ci", + ), + "upper_bound_interval": Case( + When(**{query_prefix + "upper_ci": None}, then=query_prefix + "upper_range"), + default=query_prefix + "upper_ci", + ), + "p_value_qualifier_display": sql_display( + query_prefix + "p_value_qualifier", constants.PValueQualifier + ), + "main_finding_support_display": sql_display( + query_prefix + "main_finding_support", constants.MainFinding + ), + } + + def prepare_df(self, df): + return self.format_time(df) + + +class CentralTendencyExport(ModelExport): + def get_value_map(self): + return { + "estimate": "estimate", + "estimate_type": "estimate_type_display", + "variance": "variance", + "variance_type": "variance_type_display", + "lower_bound_interval": "lower_bound_interval", + "upper_bound_interval": "upper_bound_interval", + "lower_ci": "lower_ci", + "upper_ci": "upper_ci", + "lower_range": "lower_range", + "upper_range": "upper_range", + } + + def get_annotation_map(self, query_prefix): + return { + "estimate_type_display": sql_display( + query_prefix + "estimate_type", constants.EstimateType + ), + "variance_type_display": sql_display( + query_prefix + "variance_type", constants.VarianceType + ), + "lower_bound_interval": Case( + When(**{query_prefix + "lower_ci": None}, then=query_prefix + "lower_range"), + default=query_prefix + "lower_ci", + ), + "upper_bound_interval": Case( + When(**{query_prefix + "upper_ci": None}, then=query_prefix + "upper_range"), + default=query_prefix + "upper_ci", + ), + } + + +class EpiExporter(Exporter): + def build_modules(self) -> list[ModelExport]: + return [ + StudyExport("study", "study_population__study"), + StudyPopulationExport("sp", "study_population"), + OutcomeExport("outcome", ""), + ExposureExport("exposure", "results__comparison_set__exposure"), + ComparisonSetExport("cs", "results__comparison_set"), + ResultMetricExport("metric", "results__metric"), + ResultExport("result", "results", exclude=("tags",)), + GroupExport("group", "results__results__group"), + GroupResultExport("result_group", "results__results"), + ] + + +class OutcomeComplete(FlatFileExporter): + """ + Returns a complete export of all data required to rebuild the the + epidemiological meta-result study type from scratch. + """ + + def build_df(self) -> pd.DataFrame: + return EpiExporter().get_df(self.queryset) + + +class EpiDataPivotExporter(Exporter): + def build_modules(self) -> list[ModelExport]: + return [ + StudyExport( + "study", + "study_population__study", + include=("id", "short_citation", "study_identifier", "published"), + ), + StudyPopulationExport( + "sp", "study_population", include=("id", "name", "age_profile", "source", "design") + ), + OutcomeExport( + "outcome", + "", + include=( + "id", + "name", + "system", + "effect", + "effect_subtype", + "diagnostic", + "age_of_measurement", + "effects", + ), + ), + ComparisonSetExport("cs", "results__comparison_set", include=("id", "name")), + ExposureExport( + "exposure", + "results__comparison_set__exposure", + include=( + "id", + "name", + "metric", + "measured", + "metric_units_name", + "age_of_exposure", + ), + ), + CentralTendencyExport( + "ct", + "results__comparison_set__exposure__central_tendencies", + include=( + "estimate", + "estimate_type", + "variance", + "variance_type", + "lower_bound_interval", + "upper_bound_interval", + "lower_ci", + "upper_ci", + "lower_range", + "upper_range", + ), + ), + ResultExport( + "result", + "results", + include=( + "id", + "name", + "population_description", + "tags", + "metric_description", + "comments", + "dose_response", + "statistical_power", + "statistical_test_results", + "ci_units", + "estimate_type", + "variance_type", + ), + ), + ResultMetricExport("metric", "results__metric", include=("name", "abbreviation")), + GroupExport( + "group", + "results__results__group", + include=("group_id", "name", "comparative_name", "numeric", "isControl"), + ), + GroupResultExport( + "result_group", + "results__results", + include=( + "id", + "n", + "estimate", + "lower_ci", + "upper_ci", + "lower_range", + "upper_range", + "lower_bound_interval", + "upper_bound_interval", + "variance", + "p_value", + "p_value_qualifier", + "is_main_finding", + "main_finding_support", + ), + ), + ] class OutcomeDataPivot(FlatFileExporter): - def _get_header_row(self): - if self.queryset.first() is None: - self.rob_headers, self.rob_data = {}, {} - else: - outcome_ids = set(self.queryset.values_list("id", flat=True)) - self.rob_headers, self.rob_data = FinalRiskOfBiasScore.get_dp_export( - self.queryset.first().assessment_id, - outcome_ids, - "epi", + def _add_percent_control(self, df: pd.DataFrame) -> pd.DataFrame: + def _get_stdev(x: pd.Series): + return models.GroupResult.stdev( + x["result-variance_type"], x["result_group-variance"], x["result_group-n"] ) - headers = [ - "study id", - "study name", - "study identifier", - "study published", - "study population id", - "study population name", - "study population age profile", - "study population source", - "design", - "outcome id", - "outcome name", - "outcome system", - "outcome effect", - "outcome effect subtype", - "diagnostic", - "age of outcome measurement", - "tags", - ] + def _apply_results(_df1: pd.DataFrame): + controls = _df1.loc[_df1["group-isControl"] == True] # noqa: E712 + control = _df1.iloc[0] if controls.empty else controls.iloc[0] + n_1 = control["result_group-n"] + mu_1 = control["result_group-estimate"] + sd_1 = _get_stdev(control) + + def _apply_result_groups(_df2: pd.DataFrame): + row = _df2.iloc[0] + if control["result-estimate_type"] in ["median", "mean"] and control[ + "result-variance_type" + ] in ["SD", "SE", "SEM"]: + n_2 = row["result_group-n"] + mu_2 = row["result_group-estimate"] + sd_2 = _get_stdev(row) + mean, low, high = percent_control(n_1, mu_1, sd_1, n_2, mu_2, sd_2) + return pd.DataFrame( + [[mean, low, high]], + columns=[ + "percent control mean", + "percent control low", + "percent control high", + ], + index=[row["result_group-id"]], + ) + return pd.DataFrame( + [], + columns=[ + "percent control mean", + "percent control low", + "percent control high", + ], + ) + + rgs = _df1.groupby("result_group-id", group_keys=False) + return rgs.apply(_apply_result_groups) - headers.extend(list(self.rob_headers.values())) - - headers.extend( - [ - "comparison set id", - "comparison set name", - "exposure id", - "exposure name", - "exposure metric", - "exposure measured", - "dose units", - "age of exposure", - "exposure estimate", - "exposure estimate type", - "exposure variance", - "exposure variance type", - "exposure lower bound interval", - "exposure upper bound interval", - "exposure lower ci", - "exposure upper ci", - "exposure lower range", - "exposure upper range", - "result id", - "result name", - "result population description", - "result tags", - "statistical metric", - "statistical metric abbreviation", - "statistical metric description", - "result summary", - "dose response", - "statistical power", - "statistical test results", - "CI units", - "exposure group order", - "exposure group name", - "exposure group comparison name", - "exposure group numeric", - "Reference/Exposure group", - "Result, summary numerical", - "key", - "result group id", - "N", - "estimate", - "lower CI", - "upper CI", - "lower range", - "upper range", - "lower bound interval", - "upper bound interval", - "variance", - "statistical significance", - "statistical significance (numeric)", - "main finding", - "main finding support", - "percent control mean", - "percent control low", - "percent control high", - ] + results = df.groupby("result-id", group_keys=False) + computed_df = results.apply(_apply_results) + return df.join(computed_df, on="result_group-id").drop( + columns=["result-estimate_type", "result-variance_type", "group-isControl"] ) - return headers - - def _get_data_rows(self): - rows = [] - for obj in self.queryset: - ser = obj.get_json(json_encode=False) - row = [ - ser["study_population"]["study"]["id"], - ser["study_population"]["study"]["short_citation"], - ser["study_population"]["study"]["study_identifier"], - ser["study_population"]["study"]["published"], - ser["study_population"]["id"], - ser["study_population"]["name"], - ser["study_population"]["age_profile"], - ser["study_population"]["source"], - ser["study_population"]["design"], - ser["id"], - ser["name"], - ser["system"], - ser["effect"], - ser["effect_subtype"], - ser["diagnostic"], - ser["age_of_measurement"], - self.get_flattened_tags(ser, "effects"), - ] - outcome_robs = [ - self.rob_data[(ser["id"], metric_id)] for metric_id in self.rob_headers.keys() - ] - row.extend(outcome_robs) - - for res in ser["results"]: - row_copy = list(row) - - # comparison set - row_copy.extend([res["comparison_set"]["id"], res["comparison_set"]["name"]]) - - # exposure (may be missing) - if res["comparison_set"]["exposure"]: - row_copy.extend( - [ - res["comparison_set"]["exposure"]["id"], - res["comparison_set"]["exposure"]["name"], - res["comparison_set"]["exposure"]["metric"], - res["comparison_set"]["exposure"]["measured"], - res["comparison_set"]["exposure"]["metric_units"]["name"], - res["comparison_set"]["exposure"]["age_of_exposure"], - ] - ) + def build_df(self) -> pd.DataFrame: + df = EpiDataPivotExporter().get_df(self.queryset.order_by("id", "results__results")) + outcome_ids = list(df["outcome-id"].unique()) + rob_headers, rob_data = FinalRiskOfBiasScore.get_dp_export( + self.queryset.first().assessment_id, + outcome_ids, + "epi", + ) + rob_df = pd.DataFrame( + data=[ + [rob_data[(outcome_id, metric_id)] for metric_id in rob_headers.keys()] + for outcome_id in outcome_ids + ], + columns=list(rob_headers.values()), + index=outcome_ids, + ) + df = df.join(rob_df, on="outcome-id") - num_rows_for_ct = len(res["comparison_set"]["exposure"]["central_tendencies"]) - if num_rows_for_ct == 0: - row_copy.extend(["-"] * 10) - self.addOutcomesAndGroupsToRowAndAppend(rows, res, ser, row_copy) - else: - for ct in res["comparison_set"]["exposure"]["central_tendencies"]: - row_copy_ct = list(row_copy) - row_copy_ct.extend( - [ - ct["estimate"], - ct["estimate_type"], - ct["variance"], - ct["variance_type"], - ct["lower_bound_interval"], - ct["upper_bound_interval"], - ct["lower_ci"], - ct["upper_ci"], - ct["lower_range"], - ct["upper_range"], - ] - ) - self.addOutcomesAndGroupsToRowAndAppend(rows, res, ser, row_copy_ct) - - else: - row_copy.extend(["-"] * (6 + 10)) # exposure + exposure.central_tendencies - self.addOutcomesAndGroupsToRowAndAppend(rows, res, ser, row_copy) - - return rows - - def addOutcomesAndGroupsToRowAndAppend(self, rows, res, ser, row): - # outcome details - row.extend( - [ - res["id"], - res["name"], - res["population_description"], - self.get_flattened_tags(res, "resulttags"), - res["metric"]["metric"], - res["metric"]["abbreviation"], - res["metric_description"], - res["comments"], - res["dose_response"], - res["statistical_power"], - res["statistical_test_results"], - res["ci_units"], - ] + df["Reference/Exposure group"] = ( + df["study-short_citation"] + + " (" + + df["group-name"] + + ", n=" + + df["result_group-n"].astype(str) + + ")" + ) + df["Result, summary numerical"] = ( + df["result_group-estimate"].astype(str) + + " (" + + df["result_group-lower_ci"].astype(str) + + " - " + + df["result_group-upper_ci"].astype(str) + + ")" ) + df["key"] = df["result_group-id"] + df["statistical significance"] = df.apply( + lambda x: x["result_group-p_value_qualifier"] + if pd.isna(x["result_group-p_value"]) + else f"{x['result_group-p_value']:g}" + if x["result_group-p_value_qualifier"] in ["=", "-", "n.s."] + else f"{x['result_group-p_value_qualifier']}{x['result_group-p_value']:g}", + axis="columns", + ) + df = df.drop(columns="result_group-p_value_qualifier") - for rg in res["results"]: - row_copy = list(row) - row_copy.extend( - [ - rg["group"]["group_id"], - rg["group"]["name"], - rg["group"]["comparative_name"], - rg["group"]["numeric"], - f'{ser["study_population"]["study"]["short_citation"]} ({rg["group"]["name"]}, n={rg["n"]})', - f'{rg["estimate"]} ({rg["lower_ci"]} - {rg["upper_ci"]})', - rg["id"], - rg["id"], # repeat for data-pivot key - rg["n"], - rg["estimate"], - rg["lower_ci"], - rg["upper_ci"], - rg["lower_range"], - rg["upper_range"], - rg["lower_bound_interval"], - rg["upper_bound_interval"], - rg["variance"], - rg["p_value_text"], - rg["p_value"], - rg["is_main_finding"], - rg["main_finding_support"], - rg["percentControlMean"], - rg["percentControlLow"], - rg["percentControlHigh"], - ] - ) - rows.append(row_copy) + df = self._add_percent_control(df) + + df = df.rename( + columns={ + "study-id": "study id", + "study-short_citation": "study name", + "study-study_identifier": "study identifier", + "study-published": "study published", + "sp-id": "study population id", + "sp-name": "study population name", + "sp-age_profile": "study population age profile", + "sp-source": "study population source", + "sp-design": "design", + "outcome-id": "outcome id", + "outcome-name": "outcome name", + "outcome-system": "outcome system", + "outcome-effect": "outcome effect", + "outcome-effect_subtype": "outcome effect subtype", + "outcome-diagnostic": "diagnostic", + "outcome-age_of_measurement": "age of outcome measurement", + "outcome-effects": "tags", + } + ) + df = df.rename( + columns={ + "cs-id": "comparison set id", + "cs-name": "comparison set name", + "exposure-id": "exposure id", + "exposure-name": "exposure name", + "exposure-metric": "exposure metric", + "exposure-measured": "exposure measured", + "exposure-metric_units_name": "dose units", + "exposure-age_of_exposure": "age of exposure", + "ct-estimate": "exposure estimate", + "ct-estimate_type": "exposure estimate type", + "ct-variance": "exposure variance", + "ct-variance_type": "exposure variance type", + "ct-lower_bound_interval": "exposure lower bound interval", + "ct-upper_bound_interval": "exposure upper bound interval", + "ct-lower_ci": "exposure lower ci", + "ct-upper_ci": "exposure upper ci", + "ct-lower_range": "exposure lower range", + "ct-upper_range": "exposure upper range", + "result-id": "result id", + "result-name": "result name", + "result-population_description": "result population description", + "result-tags": "result tags", + "metric-name": "statistical metric", + "metric-abbreviation": "statistical metric abbreviation", + "result-metric_description": "statistical metric description", + "result-comments": "result summary", + "result-dose_response": "dose response", + "result-statistical_power": "statistical power", + "result-statistical_test_results": "statistical test results", + "result-ci_units": "CI units", + "group-group_id": "exposure group order", + "group-name": "exposure group name", + "group-comparative_name": "exposure group comparison name", + "group-numeric": "exposure group numeric", + "result_group-id": "result group id", + "result_group-n": "N", + "result_group-estimate": "estimate", + "result_group-lower_ci": "lower CI", + "result_group-upper_ci": "upper CI", + "result_group-lower_range": "lower range", + "result_group-upper_range": "upper range", + "result_group-lower_bound_interval": "lower bound interval", + "result_group-upper_bound_interval": "upper bound interval", + "result_group-variance": "variance", + "result_group-p_value": "statistical significance (numeric)", + "result_group-is_main_finding": "main finding", + "result_group-main_finding_support": "main finding support", + } + ) + + return df diff --git a/hawc/apps/epi/models.py b/hawc/apps/epi/models.py index b7eab9f6ac..e73b0549b0 100644 --- a/hawc/apps/epi/models.py +++ b/hawc/apps/epi/models.py @@ -206,57 +206,6 @@ class StudyPopulation(models.Model): BREADCRUMB_PARENT = "study" - @staticmethod - def flat_complete_header_row(): - return ( - "sp-id", - "sp-url", - "sp-name", - "sp-design", - "sp-age_profile", - "sp-source", - "sp-countries", - "sp-region", - "sp-state", - "sp-eligible_n", - "sp-invited_n", - "sp-participant_n", - "sp-inclusion_criteria", - "sp-exclusion_criteria", - "sp-confounding_criteria", - "sp-comments", - "sp-created", - "sp-last_updated", - ) - - @staticmethod - def flat_complete_data_row(ser): - def getCriteriaList(lst, filt): - return "|".join( - [d["description"] for d in [d for d in lst if d["criteria_type"] == filt]] - ) - - return ( - ser["id"], - ser["url"], - ser["name"], - ser["design"], - ser["age_profile"], - ser["source"], - "|".join([c["name"] for c in ser["countries"]]), - ser["region"], - ser["state"], - ser["eligible_n"], - ser["invited_n"], - ser["participant_n"], - getCriteriaList(ser["criteria"], "Inclusion"), - getCriteriaList(ser["criteria"], "Exclusion"), - getCriteriaList(ser["criteria"], "Confounding"), - ser["comments"], - ser["created"], - ser["last_updated"], - ) - class Meta: ordering = ("name",) @@ -379,44 +328,6 @@ def get_absolute_url(self): def can_create_sets(self): return not self.study_population.can_create_sets() - @staticmethod - def flat_complete_header_row(): - return ( - "outcome-id", - "outcome-url", - "outcome-name", - "outcome-effects", - "outcome-system", - "outcome-effect", - "outcome-effect_subtype", - "outcome-diagnostic", - "outcome-diagnostic_description", - "outcome-age_of_measurement", - "outcome-outcome_n", - "outcome-summary", - "outcome-created", - "outcome-last_updated", - ) - - @staticmethod - def flat_complete_data_row(ser): - return ( - ser["id"], - ser["url"], - ser["name"], - "|".join([str(d["name"]) for d in ser["effects"]]), - ser["system"], - ser["effect"], - ser["effect_subtype"], - ser["diagnostic"], - ser["diagnostic_description"], - ser["age_of_measurement"], - ser["outcome_n"], - ser["summary"], - ser["created"], - ser["last_updated"], - ) - def get_study(self): return self.study_population.get_study() @@ -486,28 +397,6 @@ def get_assessment(self): def __str__(self): return self.name - @staticmethod - def flat_complete_header_row(): - return ( - "cs-id", - "cs-url", - "cs-name", - "cs-description", - "cs-created", - "cs-last_updated", - ) - - @staticmethod - def flat_complete_data_row(ser): - return ( - ser["id"], - ser["url"], - ser["name"], - ser["description"], - ser["created"], - ser["last_updated"], - ) - def get_study(self): if self.study_population: return self.study_population.get_study() @@ -590,44 +479,6 @@ def get_assessment(self): def __str__(self): return self.name - @staticmethod - def flat_complete_header_row(): - return ( - "group-id", - "group-group_id", - "group-name", - "group-numeric", - "group-comparative_name", - "group-sex", - "group-ethnicities", - "group-eligible_n", - "group-invited_n", - "group-participant_n", - "group-isControl", - "group-comments", - "group-created", - "group-last_updated", - ) - - @staticmethod - def flat_complete_data_row(ser): - return ( - ser["id"], - ser["group_id"], - ser["name"], - ser["numeric"], - ser["comparative_name"], - ser["sex"], - "|".join([d["name"] for d in ser["ethnicities"]]), - ser["eligible_n"], - ser["invited_n"], - ser["participant_n"], - ser["isControl"], - ser["comments"], - ser["created"], - ser["last_updated"], - ) - class Exposure(models.Model): objects = managers.ExposureManager() @@ -771,65 +622,6 @@ def get_absolute_url(self): def delete_caches(cls, ids): SerializerHelper.delete_caches(cls, ids) - @staticmethod - def flat_complete_header_row(): - return ( - "exposure-id", - "exposure-url", - "exposure-name", - "exposure-inhalation", - "exposure-dermal", - "exposure-oral", - "exposure-in_utero", - "exposure-iv", - "exposure-unknown_route", - "exposure-measured", - "exposure-metric", - "exposure-metric_units_id", - "exposure-metric_units_name", - "exposure-metric_description", - "exposure-analytical_method", - "exposure-sampling_period", - "exposure-age_of_exposure", - "exposure-duration", - "exposure-n", - "exposure-exposure_distribution", - "exposure-description", - "exposure-created", - "exposure-last_updated", - ) - - @staticmethod - def flat_complete_data_row(ser): - if ser is None: - ser = {} - units = ser.get("metric_units", {}) - return ( - ser.get("id"), - ser.get("url"), - ser.get("name"), - ser.get("inhalation"), - ser.get("dermal"), - ser.get("oral"), - ser.get("in_utero"), - ser.get("iv"), - ser.get("unknown_route"), - ser.get("measured"), - ser.get("metric"), - units.get("id"), - units.get("name"), - ser.get("metric_description"), - ser.get("analytical_method"), - ser.get("sampling_period"), - ser.get("age_of_exposure"), - ser.get("duration"), - ser.get("n"), - ser.get("exposure_distribution"), - ser.get("description"), - ser.get("created"), - ser.get("last_updated"), - ) - def get_study(self): return self.study_population.get_study() @@ -891,42 +683,6 @@ class Meta: def __str__(self): return f"{{CT id={self.id}, exposure={self.exposure}}}" - @staticmethod - def flat_complete_header_row(): - return ( - "central_tendency-id", - "central_tendency-estimate", - "central_tendency-estimate_type", - "central_tendency-variance", - "central_tendency-variance_type", - "central_tendency-lower_ci", - "central_tendency-upper_ci", - "central_tendency-lower_range", - "central_tendency-upper_range", - "central_tendency-description", - "central_tendency-lower_bound_interval", - "central_tendency-upper_bound_interval", - ) - - @staticmethod - def flat_complete_data_row(ser): - if ser is None: - ser = {} - return ( - ser.get("id"), - ser.get("estimate"), - ser.get("estimate_type"), - ser.get("variance"), - ser.get("variance_type"), - ser.get("lower_ci"), - ser.get("upper_ci"), - ser.get("lower_range"), - ser.get("upper_range"), - ser.get("description"), - ser.get("lower_bound_interval"), - ser.get("upper_bound_interval"), - ) - class GroupNumericalDescriptions(models.Model): objects = managers.GroupNumericalDescriptionsManager() @@ -1131,72 +887,6 @@ def get_assessment(self): def get_absolute_url(self): return reverse("epi:result_detail", args=(self.pk,)) - @staticmethod - def flat_complete_header_row(): - return ( - "metric-id", - "metric-name", - "metric-abbreviation", - "result-id", - "result-name", - "result-metric_description", - "result-metric_units", - "result-data_location", - "result-population_description", - "result-dose_response", - "result-dose_response_details", - "result-prevalence_incidence", - "result-statistical_power", - "result-statistical_power_details", - "result-statistical_test_results", - "result-trend_test", - "result-adjustment_factors", - "result-adjustment_factors_considered", - "result-estimate_type", - "result-variance_type", - "result-ci_units", - "result-comments", - "result-created", - "result-last_updated", - ) - - @staticmethod - def flat_complete_data_row(ser): - def getFactorList(lst, isIncluded): - return "|".join( - [ - d["description"] - for d in [d for d in lst if d["included_in_final_model"] == isIncluded] - ] - ) - - return ( - ser["metric"]["id"], - ser["metric"]["metric"], - ser["metric"]["abbreviation"], - ser["id"], - ser["name"], - ser["metric_description"], - ser["metric_units"], - ser["data_location"], - ser["population_description"], - ser["dose_response"], - ser["dose_response_details"], - ser["prevalence_incidence"], - ser["statistical_power"], - ser["statistical_power_details"], - ser["statistical_test_results"], - ser["trend_test"], - getFactorList(ser["factors"], True), - getFactorList(ser["factors"], False), - ser["estimate_type"], - ser["variance_type"], - ser["ci_units"], - ser["comments"], - ser["created"], - ser["last_updated"], - ) - def get_study(self): return self.outcome.get_study() @@ -1426,48 +1116,6 @@ def lower_bound_interval(self): def upper_bound_interval(self): return self.upper_range if self.upper_ci is None else self.upper_ci - @staticmethod - def flat_complete_header_row(): - return ( - "result_group-id", - "result_group-n", - "result_group-estimate", - "result_group-variance", - "result_group-lower_ci", - "result_group-upper_ci", - "result_group-lower_range", - "result_group-upper_range", - "result_group-lower_bound_interval", - "result_group-upper_bound_interval", - "result_group-p_value_qualifier", - "result_group-p_value", - "result_group-is_main_finding", - "result_group-main_finding_support", - "result_group-created", - "result_group-last_updated", - ) - - @staticmethod - def flat_complete_data_row(ser): - return ( - ser["id"], - ser["n"], - ser["estimate"], - ser["variance"], - ser["lower_ci"], - ser["upper_ci"], - ser["lower_range"], - ser["upper_range"], - ser["lower_bound_interval"], - ser["upper_bound_interval"], - ser["p_value_qualifier_display"], - ser["p_value"], - ser["is_main_finding"], - ser["main_finding_support"], - ser["created"], - ser["last_updated"], - ) - @staticmethod def stdev(variance_type, variance, n): # calculate stdev given re diff --git a/hawc/apps/study/exports.py b/hawc/apps/study/exports.py index 3e6f1a4a8b..523206e47f 100644 --- a/hawc/apps/study/exports.py +++ b/hawc/apps/study/exports.py @@ -3,6 +3,7 @@ from django.db.models import Q from ..common.exports import ModelExport +from ..common.helper import cleanHTML from ..common.models import sql_display, sql_format, str_m2m from ..lit.constants import ReferenceDatabase from .constants import CoiReported @@ -53,8 +54,18 @@ def get_annotation_map(self, query_prefix): } def prepare_df(self, df): + # cast from string to nullable int for key in [self.get_column_name("pubmed_id"), self.get_column_name("hero_id")]: - df[key] = pd.to_numeric(df[key], errors="coerce") - for key in [self.get_column_name("doi")]: - df[key] = df[key].replace("", np.nan) + if key in df.columns: + df[key] = pd.to_numeric(df[key], errors="coerce") + + # cast from string to null + doi = self.get_column_name("doi") + if doi in df.columns: + df[doi] = df[doi].replace("", np.nan) + + # clean html text + summary = self.get_column_name("summary") + if summary in df.columns: + df.loc[:, summary] = df[summary].apply(cleanHTML) return df diff --git a/tests/data/api/api-dp-data-epi.json b/tests/data/api/api-dp-data-epi.json index 0e90856514..e30adfc574 100644 --- a/tests/data/api/api-dp-data-epi.json +++ b/tests/data/api/api-dp-data-epi.json @@ -52,7 +52,7 @@ "result name": "partial PTSD", "result population description": "", "result summary": "", - "result tags": "|tag2|", + "result tags": "tag2", "statistical metric": "other", "statistical metric abbreviation": "oth", "statistical metric description": "count", @@ -68,7 +68,7 @@ "study population name": "Tokyo subway victims", "study population source": "", "study published": true, - "tags": "|tag2|", + "tags": "tag2", "upper CI": null, "upper bound interval": null, "upper range": null, @@ -127,7 +127,7 @@ "result name": "partial PTSD", "result population description": "", "result summary": "", - "result tags": "|tag2|", + "result tags": "tag2", "statistical metric": "other", "statistical metric abbreviation": "oth", "statistical metric description": "count", @@ -143,7 +143,7 @@ "study population name": "Tokyo subway victims", "study population source": "", "study published": true, - "tags": "|tag2|", + "tags": "tag2", "upper CI": null, "upper bound interval": null, "upper range": null, @@ -202,7 +202,7 @@ "result name": "partial PTSD", "result population description": "", "result summary": "", - "result tags": "|tag2|", + "result tags": "tag2", "statistical metric": "other", "statistical metric abbreviation": "oth", "statistical metric description": "count", @@ -218,7 +218,7 @@ "study population name": "Tokyo subway victims", "study population source": "", "study published": true, - "tags": "|tag2|", + "tags": "tag2", "upper CI": null, "upper bound interval": null, "upper range": null, diff --git a/tests/data/api/api-epi-assessment-export.json b/tests/data/api/api-epi-assessment-export.json index 5efd59de66..d905d935c1 100644 --- a/tests/data/api/api-epi-assessment-export.json +++ b/tests/data/api/api-epi-assessment-export.json @@ -1,14 +1,14 @@ [ { - "cs-created": "2020-05-10T22:14:03.887387-04:00", + "cs-created": "2020-05-10T22:14:03.887387-0400", "cs-description": "sarin released at five points in the Tokyo subway systems", "cs-id": 1, - "cs-last_updated": "2020-05-10T22:14:17.353405-04:00", + "cs-last_updated": "2020-05-10T22:14:17.353405-0400", "cs-name": "Tokyo subway victims (different groups)", "cs-url": "/epi/comparison-set/1/", "exposure-age_of_exposure": "", "exposure-analytical_method": "NA", - "exposure-created": "2020-05-10T22:11:48.360112-04:00", + "exposure-created": "2020-05-10T22:11:48.360112-0400", "exposure-dermal": false, "exposure-description": "", "exposure-duration": "", @@ -17,7 +17,7 @@ "exposure-in_utero": false, "exposure-inhalation": true, "exposure-iv": false, - "exposure-last_updated": "2020-05-10T22:11:48.360136-04:00", + "exposure-last_updated": "2020-05-10T22:11:48.360136-0400", "exposure-measured": "Sarin", "exposure-metric": "air", "exposure-metric_description": "sarin released at five points in the Tokyo subway systems", @@ -31,14 +31,14 @@ "exposure-url": "/epi/exposure/1/", "group-comments": "", "group-comparative_name": "", - "group-created": "2020-05-10T22:14:04.009669-04:00", + "group-created": "2020-05-10T22:14:04.009669-0400", "group-eligible_n": 582, "group-ethnicities": "Asian", "group-group_id": 0, "group-id": 1, "group-invited_n": null, "group-isControl": null, - "group-last_updated": "2020-05-10T22:14:17.432631-04:00", + "group-last_updated": "2020-05-10T22:14:17.432631-0400", "group-name": "Tokyo (St. Luke) respondents 1997", "group-numeric": null, "group-participant_n": 283, @@ -47,14 +47,14 @@ "metric-id": 2, "metric-name": "other", "outcome-age_of_measurement": "", - "outcome-created": "2020-05-10T22:21:56.870317-04:00", + "outcome-created": "2020-05-10T22:21:56.870317-0400", "outcome-diagnostic": "other", "outcome-diagnostic_description": "NR", "outcome-effect": "neurological: behavior", "outcome-effect_subtype": "", "outcome-effects": "tag2", "outcome-id": 4, - "outcome-last_updated": "2020-05-10T22:21:56.870345-04:00", + "outcome-last_updated": "2020-05-10T22:21:56.870345-0400", "outcome-name": "partial PTSD", "outcome-outcome_n": null, "outcome-summary": "", @@ -64,13 +64,13 @@ "result-adjustment_factors_considered": "", "result-ci_units": 0.95, "result-comments": "", - "result-created": "2020-05-10T22:23:49.490734-04:00", + "result-created": "2020-05-10T22:23:49.490734-0400", "result-data_location": "Table 2", "result-dose_response": "not-applicable", "result-dose_response_details": "", "result-estimate_type": "---", "result-id": 1, - "result-last_updated": "2020-05-10T22:25:38.225671-04:00", + "result-last_updated": "2020-05-10T22:25:38.225671-0400", "result-metric_description": "count", "result-metric_units": "#", "result-name": "partial PTSD", @@ -81,11 +81,11 @@ "result-statistical_test_results": "", "result-trend_test": "", "result-variance_type": "---", - "result_group-created": "2020-05-10T22:23:49.737387-04:00", + "result_group-created": "2020-05-10T22:23:49.737387-0400", "result_group-estimate": 20.0, "result_group-id": 1, "result_group-is_main_finding": false, - "result_group-last_updated": "2020-05-10T22:23:49.737414-04:00", + "result_group-last_updated": "2020-05-10T22:23:49.737414-0400", "result_group-lower_bound_interval": null, "result_group-lower_ci": null, "result_group-lower_range": null, @@ -101,14 +101,14 @@ "sp-comments": "Descriptions.", "sp-confounding_criteria": "have an exposure timing during the periconceptional period or during pregnancy", "sp-countries": "Japan", - "sp-created": "2020-05-10T22:09:54.288594-04:00", + "sp-created": "2020-05-10T22:09:54.288594-0400", "sp-design": "Case series", "sp-eligible_n": null, "sp-exclusion_criteria": "include an evaluation of the direct association between folic acid exposure and one of the outcomes of interest", "sp-id": 1, "sp-inclusion_criteria": "\"heterogeneity of exposure and outcome\" excluded 9 of the 14 studies that met the inclusion criteria", "sp-invited_n": null, - "sp-last_updated": "2020-05-10T22:09:54.288617-04:00", + "sp-last_updated": "2020-05-10T22:09:54.288617-0400", "sp-name": "Tokyo subway victims", "sp-participant_n": 582, "sp-region": "Tokyo", @@ -138,15 +138,15 @@ "study-url": "/study/5/" }, { - "cs-created": "2020-05-10T22:14:03.887387-04:00", + "cs-created": "2020-05-10T22:14:03.887387-0400", "cs-description": "sarin released at five points in the Tokyo subway systems", "cs-id": 1, - "cs-last_updated": "2020-05-10T22:14:17.353405-04:00", + "cs-last_updated": "2020-05-10T22:14:17.353405-0400", "cs-name": "Tokyo subway victims (different groups)", "cs-url": "/epi/comparison-set/1/", "exposure-age_of_exposure": "", "exposure-analytical_method": "NA", - "exposure-created": "2020-05-10T22:11:48.360112-04:00", + "exposure-created": "2020-05-10T22:11:48.360112-0400", "exposure-dermal": false, "exposure-description": "", "exposure-duration": "", @@ -155,7 +155,7 @@ "exposure-in_utero": false, "exposure-inhalation": true, "exposure-iv": false, - "exposure-last_updated": "2020-05-10T22:11:48.360136-04:00", + "exposure-last_updated": "2020-05-10T22:11:48.360136-0400", "exposure-measured": "Sarin", "exposure-metric": "air", "exposure-metric_description": "sarin released at five points in the Tokyo subway systems", @@ -169,14 +169,14 @@ "exposure-url": "/epi/exposure/1/", "group-comments": "", "group-comparative_name": "", - "group-created": "2020-05-10T22:14:04.247759-04:00", + "group-created": "2020-05-10T22:14:04.247759-0400", "group-eligible_n": 582, "group-ethnicities": "Asian", "group-group_id": 1, "group-id": 2, "group-invited_n": null, "group-isControl": null, - "group-last_updated": "2020-05-10T22:14:17.591544-04:00", + "group-last_updated": "2020-05-10T22:14:17.591544-0400", "group-name": "Tokyo (St. Luke) respondents 1998", "group-numeric": null, "group-participant_n": 206, @@ -185,14 +185,14 @@ "metric-id": 2, "metric-name": "other", "outcome-age_of_measurement": "", - "outcome-created": "2020-05-10T22:21:56.870317-04:00", + "outcome-created": "2020-05-10T22:21:56.870317-0400", "outcome-diagnostic": "other", "outcome-diagnostic_description": "NR", "outcome-effect": "neurological: behavior", "outcome-effect_subtype": "", "outcome-effects": "tag2", "outcome-id": 4, - "outcome-last_updated": "2020-05-10T22:21:56.870345-04:00", + "outcome-last_updated": "2020-05-10T22:21:56.870345-0400", "outcome-name": "partial PTSD", "outcome-outcome_n": null, "outcome-summary": "", @@ -202,13 +202,13 @@ "result-adjustment_factors_considered": "", "result-ci_units": 0.95, "result-comments": "", - "result-created": "2020-05-10T22:23:49.490734-04:00", + "result-created": "2020-05-10T22:23:49.490734-0400", "result-data_location": "Table 2", "result-dose_response": "not-applicable", "result-dose_response_details": "", "result-estimate_type": "---", "result-id": 1, - "result-last_updated": "2020-05-10T22:25:38.225671-04:00", + "result-last_updated": "2020-05-10T22:25:38.225671-0400", "result-metric_description": "count", "result-metric_units": "#", "result-name": "partial PTSD", @@ -219,11 +219,11 @@ "result-statistical_test_results": "", "result-trend_test": "", "result-variance_type": "---", - "result_group-created": "2020-05-10T22:23:49.806018-04:00", + "result_group-created": "2020-05-10T22:23:49.806018-0400", "result_group-estimate": 15.0, "result_group-id": 2, "result_group-is_main_finding": false, - "result_group-last_updated": "2020-05-10T22:23:49.806044-04:00", + "result_group-last_updated": "2020-05-10T22:23:49.806044-0400", "result_group-lower_bound_interval": null, "result_group-lower_ci": null, "result_group-lower_range": null, @@ -239,14 +239,14 @@ "sp-comments": "Descriptions.", "sp-confounding_criteria": "have an exposure timing during the periconceptional period or during pregnancy", "sp-countries": "Japan", - "sp-created": "2020-05-10T22:09:54.288594-04:00", + "sp-created": "2020-05-10T22:09:54.288594-0400", "sp-design": "Case series", "sp-eligible_n": null, "sp-exclusion_criteria": "include an evaluation of the direct association between folic acid exposure and one of the outcomes of interest", "sp-id": 1, "sp-inclusion_criteria": "\"heterogeneity of exposure and outcome\" excluded 9 of the 14 studies that met the inclusion criteria", "sp-invited_n": null, - "sp-last_updated": "2020-05-10T22:09:54.288617-04:00", + "sp-last_updated": "2020-05-10T22:09:54.288617-0400", "sp-name": "Tokyo subway victims", "sp-participant_n": 582, "sp-region": "Tokyo", @@ -276,15 +276,15 @@ "study-url": "/study/5/" }, { - "cs-created": "2020-05-10T22:14:03.887387-04:00", + "cs-created": "2020-05-10T22:14:03.887387-0400", "cs-description": "sarin released at five points in the Tokyo subway systems", "cs-id": 1, - "cs-last_updated": "2020-05-10T22:14:17.353405-04:00", + "cs-last_updated": "2020-05-10T22:14:17.353405-0400", "cs-name": "Tokyo subway victims (different groups)", "cs-url": "/epi/comparison-set/1/", "exposure-age_of_exposure": "", "exposure-analytical_method": "NA", - "exposure-created": "2020-05-10T22:11:48.360112-04:00", + "exposure-created": "2020-05-10T22:11:48.360112-0400", "exposure-dermal": false, "exposure-description": "", "exposure-duration": "", @@ -293,7 +293,7 @@ "exposure-in_utero": false, "exposure-inhalation": true, "exposure-iv": false, - "exposure-last_updated": "2020-05-10T22:11:48.360136-04:00", + "exposure-last_updated": "2020-05-10T22:11:48.360136-0400", "exposure-measured": "Sarin", "exposure-metric": "air", "exposure-metric_description": "sarin released at five points in the Tokyo subway systems", @@ -307,14 +307,14 @@ "exposure-url": "/epi/exposure/1/", "group-comments": "", "group-comparative_name": "", - "group-created": "2020-05-10T22:14:04.403130-04:00", + "group-created": "2020-05-10T22:14:04.403130-0400", "group-eligible_n": 582, "group-ethnicities": "Asian", "group-group_id": 2, "group-id": 3, "group-invited_n": null, "group-isControl": null, - "group-last_updated": "2020-05-10T22:14:17.746856-04:00", + "group-last_updated": "2020-05-10T22:14:17.746856-0400", "group-name": "Tokyo (St. Luke) respondents 2000", "group-numeric": null, "group-participant_n": 191, @@ -323,14 +323,14 @@ "metric-id": 2, "metric-name": "other", "outcome-age_of_measurement": "", - "outcome-created": "2020-05-10T22:21:56.870317-04:00", + "outcome-created": "2020-05-10T22:21:56.870317-0400", "outcome-diagnostic": "other", "outcome-diagnostic_description": "NR", "outcome-effect": "neurological: behavior", "outcome-effect_subtype": "", "outcome-effects": "tag2", "outcome-id": 4, - "outcome-last_updated": "2020-05-10T22:21:56.870345-04:00", + "outcome-last_updated": "2020-05-10T22:21:56.870345-0400", "outcome-name": "partial PTSD", "outcome-outcome_n": null, "outcome-summary": "", @@ -340,13 +340,13 @@ "result-adjustment_factors_considered": "", "result-ci_units": 0.95, "result-comments": "", - "result-created": "2020-05-10T22:23:49.490734-04:00", + "result-created": "2020-05-10T22:23:49.490734-0400", "result-data_location": "Table 2", "result-dose_response": "not-applicable", "result-dose_response_details": "", "result-estimate_type": "---", "result-id": 1, - "result-last_updated": "2020-05-10T22:25:38.225671-04:00", + "result-last_updated": "2020-05-10T22:25:38.225671-0400", "result-metric_description": "count", "result-metric_units": "#", "result-name": "partial PTSD", @@ -357,11 +357,11 @@ "result-statistical_test_results": "", "result-trend_test": "", "result-variance_type": "---", - "result_group-created": "2020-05-10T22:23:49.859670-04:00", + "result_group-created": "2020-05-10T22:23:49.859670-0400", "result_group-estimate": 16.0, "result_group-id": 3, "result_group-is_main_finding": false, - "result_group-last_updated": "2020-05-10T22:23:49.859696-04:00", + "result_group-last_updated": "2020-05-10T22:23:49.859696-0400", "result_group-lower_bound_interval": null, "result_group-lower_ci": null, "result_group-lower_range": null, @@ -377,14 +377,14 @@ "sp-comments": "Descriptions.", "sp-confounding_criteria": "have an exposure timing during the periconceptional period or during pregnancy", "sp-countries": "Japan", - "sp-created": "2020-05-10T22:09:54.288594-04:00", + "sp-created": "2020-05-10T22:09:54.288594-0400", "sp-design": "Case series", "sp-eligible_n": null, "sp-exclusion_criteria": "include an evaluation of the direct association between folic acid exposure and one of the outcomes of interest", "sp-id": 1, "sp-inclusion_criteria": "\"heterogeneity of exposure and outcome\" excluded 9 of the 14 studies that met the inclusion criteria", "sp-invited_n": null, - "sp-last_updated": "2020-05-10T22:09:54.288617-04:00", + "sp-last_updated": "2020-05-10T22:09:54.288617-0400", "sp-name": "Tokyo subway victims", "sp-participant_n": 582, "sp-region": "Tokyo", From ce11ca0202052fa2b4b1ae07da084aad5d33ac7c Mon Sep 17 00:00:00 2001 From: Andy Shapiro Date: Thu, 12 Oct 2023 18:58:58 -0400 Subject: [PATCH 02/11] rewrite riskofbias exports (#921) * preliminary epi rewrite * changes * changes * changes * fix * added back something accidentally deleted * moved code * fix test * remove old stuff * cleanup: * rewrite riskofbias exports * update naming for domain and metric --------- Co-authored-by: Daniel Rabstejnek --- hawc/apps/common/exports.py | 6 +- hawc/apps/riskofbias/api.py | 24 +- .../exports/RiskOfBiasCompleteFlatSchema.tsv | 26 +- .../data/exports/RiskOfBiasFlatSchema.tsv | 26 +- hawc/apps/riskofbias/exports.py | 178 ++++---- hawc/apps/riskofbias/models.py | 57 +-- tests/data/api/api-rob-assessment-export.json | 90 ++-- .../api/api-rob-assessment-full-export.json | 390 +++++++++--------- tests/hawc/apps/riskofbias/test_exports.py | 12 +- 9 files changed, 387 insertions(+), 422 deletions(-) diff --git a/hawc/apps/common/exports.py b/hawc/apps/common/exports.py index 35da26388d..94205d5774 100644 --- a/hawc/apps/common/exports.py +++ b/hawc/apps/common/exports.py @@ -210,6 +210,10 @@ def get_df(self, qs: QuerySet) -> pd.DataFrame: df = module.prepare_df(df) return df + @classmethod + def build_metadata(cls, df: pd.DataFrame) -> pd.DataFrame | None: + return None + @classmethod def flat_export(cls, qs: QuerySet, filename: str) -> FlatExport: """Return an instance of a FlatExport. @@ -218,4 +222,4 @@ def flat_export(cls, qs: QuerySet, filename: str) -> FlatExport: filename (str): the filename for the export """ df = cls().get_df(qs) - return FlatExport(df=df, filename=filename) + return FlatExport(df=df, filename=filename, metadata=cls.build_metadata(df)) diff --git a/hawc/apps/riskofbias/api.py b/hawc/apps/riskofbias/api.py index 027a0d07d8..89ff320f2a 100644 --- a/hawc/apps/riskofbias/api.py +++ b/hawc/apps/riskofbias/api.py @@ -53,13 +53,16 @@ def export(self, request, pk): """ self.get_object() rob_name = self.assessment.get_rob_name_display().lower() - exporter = exports.RiskOfBiasFlat( - self.get_queryset().none(), + qs = models.RiskOfBiasScore.objects.filter( + riskofbias__active=True, + riskofbias__final=True, + riskofbias__study__assessment=self.assessment, + ).order_by("riskofbias__study__short_citation", "riskofbias_id", "id") + exporter = exports.RiskOfBiasExporter.flat_export( + qs, filename=f"{self.assessment}-{rob_name}", - assessment_id=self.assessment.id, ) - - return Response(exporter.build_export()) + return Response(exporter) @action( detail=True, @@ -73,12 +76,15 @@ def full_export(self, request, pk): """ self.get_object() rob_name = self.assessment.get_rob_name_display().lower() - exporter = exports.RiskOfBiasCompleteFlat( - self.get_queryset().none(), + qs = models.RiskOfBiasScore.objects.filter( + riskofbias__active=True, + riskofbias__study__assessment=self.assessment, + ).order_by("riskofbias__study__short_citation", "riskofbias_id", "id") + exporter = exports.RiskOfBiasCompleteExporter.flat_export( + qs, filename=f"{self.assessment}-{rob_name}-complete", - assessment_id=self.assessment.id, ) - return Response(exporter.build_export()) + return Response(exporter) @action(detail=False, methods=("post",), permission_classes=(IsAuthenticated,)) def bulk_rob_copy(self, request): diff --git a/hawc/apps/riskofbias/data/exports/RiskOfBiasCompleteFlatSchema.tsv b/hawc/apps/riskofbias/data/exports/RiskOfBiasCompleteFlatSchema.tsv index a5c223c08a..9c2c495903 100644 --- a/hawc/apps/riskofbias/data/exports/RiskOfBiasCompleteFlatSchema.tsv +++ b/hawc/apps/riskofbias/data/exports/RiskOfBiasCompleteFlatSchema.tsv @@ -27,16 +27,16 @@ rob-author_id The study evaluation/risk of bias author HAWC identifier rob-author_name The study evaluation/risk of bias author name rob-created The date the review was created rob-last_updated The date the review was last updated -rob-domain_id Study evaluation/risk of bias domain identifier -rob-domain_name Study evaluation/risk of bias domain name -rob-domain_description Study evaluation/risk of bias domain description -rob-metric_id Study evaluation/risk of bias metric identifier -rob-metric_name Study evaluation/risk of bias metric name -rob-metric_description Study evaluation/risk of bias metric description -rob-score_id Study evaluation/risk of bias metric response id for a unique study/metric pair -rob-score_is_default If multiple responses exist for a study/metric pair, should this one be treated as the default response -rob-score_label If multiple responses exist for a study/metric pair, a label for this response -rob-score_score If a qualitative judgment is made, an integer representation of the judgment value -rob-score_description If a qualitative judgment is made, a text label of the judgment value -rob-score_bias_direction Expected direction of bias, if used. 0= (not entered/unknown), 1=⬆ (away from null), 2=⬇ (towards null) -rob-score_notes Reviewer notes to describe the study evaluation for a study/metric pair +rob_domain-id Study evaluation/risk of bias domain identifier +rob_domain-name Study evaluation/risk of bias domain name +rob_domain-description Study evaluation/risk of bias domain description +rob_metric-id Study evaluation/risk of bias metric identifier +rob_metric-name Study evaluation/risk of bias metric name +rob_metric-description Study evaluation/risk of bias metric description +rob_score-id Study evaluation/risk of bias metric response id for a unique study/metric pair +rob_score-is_default If multiple responses exist for a study/metric pair, should this one be treated as the default response +rob_score-label If multiple responses exist for a study/metric pair, a label for this response +rob_score-score If a qualitative judgment is made, an integer representation of the judgment value +rob_score-description If a qualitative judgment is made, a text label of the judgment value +rob_score-bias_direction Expected direction of bias, if used. 0= (not entered/unknown), 1=⬆ (away from null), 2=⬇ (towards null) +rob_score-notes Reviewer notes to describe the study evaluation for a study/metric pair diff --git a/hawc/apps/riskofbias/data/exports/RiskOfBiasFlatSchema.tsv b/hawc/apps/riskofbias/data/exports/RiskOfBiasFlatSchema.tsv index 4b4c780f59..7f965771b6 100644 --- a/hawc/apps/riskofbias/data/exports/RiskOfBiasFlatSchema.tsv +++ b/hawc/apps/riskofbias/data/exports/RiskOfBiasFlatSchema.tsv @@ -23,16 +23,16 @@ study-published If True, this study, study evaluation, and extraction details ma rob-id Study evaluation/risk of bias review unique identifier rob-created The date the review was created rob-last_updated The date the review was last updated -rob-domain_id Study evaluation/risk of bias domain identifier -rob-domain_name Study evaluation/risk of bias domain name -rob-domain_description Study evaluation/risk of bias domain description -rob-metric_id Study evaluation/risk of bias metric identifier -rob-metric_name Study evaluation/risk of bias metric name -rob-metric_description Study evaluation/risk of bias metric description -rob-score_id Study evaluation/risk of bias metric response id for a unique study/metric pair -rob-score_is_default If multiple responses exist for a study/metric pair, should this one be treated as the default response -rob-score_label If multiple responses exist for a study/metric pair, a label for this response -rob-score_score If a qualitative judgment is made, an integer representation of the judgment value -rob-score_description If a qualitative judgment is made, a text label of the judgment value -rob-score_bias_direction Expected direction of bias, if used. 0= (not entered/unknown), 1=⬆ (away from null), 2=⬇ (towards null) -rob-score_notes Reviewer notes to describe the study evaluation for a study/metric pair +rob_domain-id Study evaluation/risk of bias domain identifier +rob_domain-name Study evaluation/risk of bias domain name +rob_domain-description Study evaluation/risk of bias domain description +rob_metric-id Study evaluation/risk of bias metric identifier +rob_metric-name Study evaluation/risk of bias metric name +rob_metric-description Study evaluation/risk of bias metric description +rob_score-id Study evaluation/risk of bias metric response id for a unique study/metric pair +rob_score-is_default If multiple responses exist for a study/metric pair, should this one be treated as the default response +rob_score-label If multiple responses exist for a study/metric pair, a label for this response +rob_score-score If a qualitative judgment is made, an integer representation of the judgment value +rob_score-description If a qualitative judgment is made, a text label of the judgment value +rob_score-bias_direction Expected direction of bias, if used. 0= (not entered/unknown), 1=⬆ (away from null), 2=⬇ (towards null) +rob_score-notes Reviewer notes to describe the study evaluation for a study/metric pair diff --git a/hawc/apps/riskofbias/exports.py b/hawc/apps/riskofbias/exports.py index 3ef5de6682..bba97ad943 100644 --- a/hawc/apps/riskofbias/exports.py +++ b/hawc/apps/riskofbias/exports.py @@ -1,92 +1,104 @@ import pandas as pd from django.conf import settings - -from ..common.helper import FlatFileExporter -from ..study.models import Study -from ..study.serializers import VerboseStudySerializer -from . import models, serializers - - -class RiskOfBiasFlat(FlatFileExporter): - """ - Returns a complete export of active Final Risk of Bias reviews, without - reviewer information. - """ - - final_only = True # only return final data - - def get_serialized_data(self): - assessment_id = self.kwargs["assessment_id"] - qs = ( - Study.objects.filter(assessment_id=assessment_id) - .prefetch_related("identifiers", "riskofbiases__scores__overridden_objects") - .select_related("assessment") - ) - ser = VerboseStudySerializer(qs, many=True) - study_data = ser.data - - if not self.final_only: - qs = ( - models.RiskOfBias.objects.filter(study__assessment_id=assessment_id, active=True) - .select_related("author") - .prefetch_related("scores__overridden_objects") - ) - ser = serializers.RiskOfBiasSerializer(qs, many=True) - rob_data = ser.data - for study in study_data: - study["riskofbiases"] = [rob for rob in rob_data if rob["study"] == study["id"]] - - return study_data - - def _get_header_row(self): - header = [] - header.extend(Study.flat_complete_header_row()) - header.extend(models.RiskOfBias.flat_header_row(final_only=self.final_only)) - header.extend(models.RiskOfBiasScore.flat_complete_header_row()) - return header - - def _get_data_rows(self): - rows = [] - for ser in self.get_serialized_data(): - domains = ser["rob_settings"]["domains"] - metrics = ser["rob_settings"]["metrics"] - domain_map = {domain["id"]: domain for domain in domains} - metric_map = { - metric["id"]: dict(metric, domain=domain_map[metric["domain_id"]]) - for metric in metrics - } - - row1 = [] - row1.extend(Study.flat_complete_data_row(ser)) - - robs = [rob for rob in ser.get("riskofbiases", [])] - if self.final_only: - robs = [rob for rob in robs if rob["final"] and rob["active"]] - - for rob in robs: - row2 = list(row1) - row2.extend(models.RiskOfBias.flat_data_row(rob, final_only=self.final_only)) - for score in rob["scores"]: - row3 = list(row2) - score["metric"] = metric_map[score["metric_id"]] - row3.extend(models.RiskOfBiasScore.flat_complete_data_row(score)) - rows.append(row3) - - return rows - - def build_metadata(self) -> pd.DataFrame | None: +from django.db.models import Value + +from ..common.exports import Exporter, ModelExport +from ..common.helper import cleanHTML +from ..common.models import sql_format +from ..study.exports import StudyExport +from . import constants + + +class RiskOfBiasExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "active": "active", + "final": "final", + "author_id": "author_id", + "author_name": "author_full_name", + "created": "created", + "last_updated": "last_updated", + } + + def get_annotation_map(self, query_prefix): + return { + "author_full_name": sql_format( + "{} {}", query_prefix + "author__first_name", query_prefix + "author__last_name" + ), + } + + def prepare_df(self, df): + return self.format_time(df) + + +class DomainExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "name": "name", + "description": "description", + } + + +class MetricExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "name": "name", + "description": "description", + } + + +class RiskOfBiasScoreExport(ModelExport): + def get_value_map(self): + return { + "id": "id", + "is_default": "is_default", + "label": "label", + "score": "score", + "description": Value("?"), + "bias_direction": "bias_direction", + "notes": "notes", + } + + def prepare_df(self, df: pd.DataFrame) -> pd.DataFrame: + if (key := self.get_column_name("description")) in df.columns: + df.loc[:, key] = df[self.get_column_name("score")].map(constants.SCORE_CHOICES_MAP) + if (key := self.get_column_name("notes")) in df.columns: + df.loc[:, key] = df[key].apply(cleanHTML) + return df + + +class RiskOfBiasExporter(Exporter): + def build_modules(self) -> list[ModelExport]: + return [ + StudyExport("study", "riskofbias__study"), + RiskOfBiasExport( + "rob", "riskofbias", exclude=("active", "final", "author_id", "author_name") + ), + DomainExport("rob_domain", "metric__domain"), + MetricExport("rob_metric", "metric"), + RiskOfBiasScoreExport("rob_score", ""), + ] + + @classmethod + def build_metadata(cls, df: pd.DataFrame) -> pd.DataFrame | None: fn = settings.PROJECT_PATH / "apps/riskofbias/data/exports/RiskOfBiasFlatSchema.tsv" return pd.read_csv(fn, delimiter="\t") -class RiskOfBiasCompleteFlat(RiskOfBiasFlat): - """ - Returns a complete export of all Risk of Bias reviews including reviewer - information. - """ - - final_only = False +class RiskOfBiasCompleteExporter(Exporter): + def build_modules(self) -> list[ModelExport]: + return [ + StudyExport("study", "riskofbias__study"), + RiskOfBiasExport("rob", "riskofbias"), + DomainExport("rob_domain", "metric__domain"), + MetricExport("rob_metric", "metric"), + RiskOfBiasScoreExport("rob_score", ""), + ] - def build_metadata(self) -> pd.DataFrame | None: + @classmethod + def build_metadata(cls, df: pd.DataFrame) -> pd.DataFrame | None: fn = settings.PROJECT_PATH / "apps/riskofbias/data/exports/RiskOfBiasCompleteFlatSchema.tsv" return pd.read_csv(fn, delimiter="\t") diff --git a/hawc/apps/riskofbias/models.py b/hawc/apps/riskofbias/models.py index b44980f29a..bcffce6272 100644 --- a/hawc/apps/riskofbias/models.py +++ b/hawc/apps/riskofbias/models.py @@ -14,7 +14,7 @@ from reversion import revisions as reversion from ..assessment.models import Assessment -from ..common.helper import HAWCDjangoJSONEncoder, SerializerHelper, cleanHTML +from ..common.helper import HAWCDjangoJSONEncoder, SerializerHelper from ..myuser.models import HAWCUser from ..study.models import Study from . import constants, managers @@ -268,25 +268,6 @@ def study_reviews_complete(self): def delete_caches(cls, ids): SerializerHelper.delete_caches(cls, ids) - @staticmethod - def flat_header_row(final_only: bool = True): - col = ["rob-id", "rob-created", "rob-last_updated"] - if not final_only: - col[1:1] = ["rob-active", "rob-final", "rob-author_id", "rob-author_name"] - return col - - @staticmethod - def flat_data_row(ser, final_only: bool = True): - row = [ser["id"], ser["created"], ser["last_updated"]] - if not final_only: - row[1:1] = [ - ser["active"], - ser["final"], - ser["author"]["id"], - ser["author"]["full_name"], - ] - return row - def get_override_options(self) -> dict: """Get risk of bias override options and overrides @@ -391,42 +372,6 @@ def save(self, *args, **kwargs): def get_assessment(self): return self.metric.get_assessment() - @staticmethod - def flat_complete_header_row(): - return ( - "rob-domain_id", - "rob-domain_name", - "rob-domain_description", - "rob-metric_id", - "rob-metric_name", - "rob-metric_description", - "rob-score_id", - "rob-score_is_default", - "rob-score_label", - "rob-score_score", - "rob-score_description", - "rob-score_bias_direction", - "rob-score_notes", - ) - - @staticmethod - def flat_complete_data_row(ser): - return ( - ser["metric"]["domain"]["id"], - ser["metric"]["domain"]["name"], - ser["metric"]["domain"]["description"], - ser["metric"]["id"], - ser["metric"]["name"], - ser["metric"]["description"], - ser["id"], - ser["is_default"], - ser["label"], - ser["score"], - ser["score_description"], - ser["bias_direction"], - cleanHTML(ser["notes"]), - ) - @property def score_symbol(self): return constants.SCORE_SYMBOLS[self.score] diff --git a/tests/data/api/api-rob-assessment-export.json b/tests/data/api/api-rob-assessment-export.json index 8a3d462cc8..a0b9f6233c 100644 --- a/tests/data/api/api-rob-assessment-export.json +++ b/tests/data/api/api-rob-assessment-export.json @@ -1,21 +1,21 @@ [ { - "rob-created": "2020-05-08T13:56:46.183552-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2020-05-08T13:56:46.183552-0400", "rob-id": 6, - "rob-last_updated": "2020-05-08T15:34:31.468702-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 1, - "rob-score_description": "Probably low risk of bias", - "rob-score_id": 14, - "rob-score_is_default": true, - "rob-score_label": "test1", - "rob-score_notes": "test", - "rob-score_score": 16, + "rob-last_updated": "2020-05-08T15:34:31.468702-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 1, + "rob_score-description": "Probably low risk of bias", + "rob_score-id": 14, + "rob_score-is_default": true, + "rob_score-label": "test1", + "rob_score-notes": "test", + "rob_score-score": 16, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -39,22 +39,22 @@ "study-url": "/study/7/" }, { - "rob-created": "2020-05-08T13:56:46.183552-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2020-05-08T13:56:46.183552-0400", "rob-id": 6, - "rob-last_updated": "2020-05-08T15:34:31.468702-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 1, - "rob-score_description": "Definitely high risk of bias", - "rob-score_id": 16, - "rob-score_is_default": false, - "rob-score_label": "test2", - "rob-score_notes": "beep", - "rob-score_score": 14, + "rob-last_updated": "2020-05-08T15:34:31.468702-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 11, + "rob_domain-name": "overall", + "rob_metric-description": "

overall description

", + "rob_metric-id": 15, + "rob_metric-name": "overall metric 1", + "rob_score-bias_direction": 1, + "rob_score-description": "Probably low risk of bias", + "rob_score-id": 15, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "test", + "rob_score-score": 16, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -78,22 +78,22 @@ "study-url": "/study/7/" }, { - "rob-created": "2020-05-08T13:56:46.183552-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 11, - "rob-domain_name": "overall", + "rob-created": "2020-05-08T13:56:46.183552-0400", "rob-id": 6, - "rob-last_updated": "2020-05-08T15:34:31.468702-04:00", - "rob-metric_description": "

overall description

", - "rob-metric_id": 15, - "rob-metric_name": "overall metric 1", - "rob-score_bias_direction": 1, - "rob-score_description": "Probably low risk of bias", - "rob-score_id": 15, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "test", - "rob-score_score": 16, + "rob-last_updated": "2020-05-08T15:34:31.468702-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 1, + "rob_score-description": "Definitely high risk of bias", + "rob_score-id": 16, + "rob_score-is_default": false, + "rob_score-label": "test2", + "rob_score-notes": "beep", + "rob_score-score": 14, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", diff --git a/tests/data/api/api-rob-assessment-full-export.json b/tests/data/api/api-rob-assessment-full-export.json index 52ac6c882d..46dc3c952b 100644 --- a/tests/data/api/api-rob-assessment-full-export.json +++ b/tests/data/api/api-rob-assessment-full-export.json @@ -3,23 +3,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2020-05-08T13:56:45.599914-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2020-05-08T13:56:45.599914-0400", "rob-final": false, "rob-id": 4, - "rob-last_updated": "2020-05-08T15:33:23.187120-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Definitely low risk of bias", - "rob-score_id": 10, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "test", - "rob-score_score": 17, + "rob-last_updated": "2020-05-08T15:33:23.187120-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Definitely low risk of bias", + "rob_score-id": 10, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "test", + "rob_score-score": 17, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -46,23 +46,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2020-05-08T13:56:45.599914-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 11, - "rob-domain_name": "overall", + "rob-created": "2020-05-08T13:56:45.599914-0400", "rob-final": false, "rob-id": 4, - "rob-last_updated": "2020-05-08T15:33:23.187120-04:00", - "rob-metric_description": "

overall description

", - "rob-metric_id": 15, - "rob-metric_name": "overall metric 1", - "rob-score_bias_direction": 1, - "rob-score_description": "Definitely high risk of bias", - "rob-score_id": 11, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "test2", - "rob-score_score": 14, + "rob-last_updated": "2020-05-08T15:33:23.187120-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 11, + "rob_domain-name": "overall", + "rob_metric-description": "

overall description

", + "rob_metric-id": 15, + "rob_metric-name": "overall metric 1", + "rob_score-bias_direction": 1, + "rob_score-description": "Definitely high risk of bias", + "rob_score-id": 11, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "test2", + "rob_score-score": 14, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -89,23 +89,23 @@ "rob-active": true, "rob-author_id": 2, "rob-author_name": "Project Manager", - "rob-created": "2020-05-08T13:56:45.903109-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2020-05-08T13:56:45.903109-0400", "rob-final": false, "rob-id": 5, - "rob-last_updated": "2020-05-08T15:33:43.045401-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Probably low risk of bias", - "rob-score_id": 12, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "test", - "rob-score_score": 16, + "rob-last_updated": "2020-05-08T15:33:43.045401-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Probably low risk of bias", + "rob_score-id": 12, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "test", + "rob_score-score": 16, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -132,23 +132,23 @@ "rob-active": true, "rob-author_id": 2, "rob-author_name": "Project Manager", - "rob-created": "2020-05-08T13:56:45.903109-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 11, - "rob-domain_name": "overall", + "rob-created": "2020-05-08T13:56:45.903109-0400", "rob-final": false, "rob-id": 5, - "rob-last_updated": "2020-05-08T15:33:43.045401-04:00", - "rob-metric_description": "

overall description

", - "rob-metric_id": 15, - "rob-metric_name": "overall metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Definitely low risk of bias", - "rob-score_id": 13, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "test", - "rob-score_score": 17, + "rob-last_updated": "2020-05-08T15:33:43.045401-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 11, + "rob_domain-name": "overall", + "rob_metric-description": "

overall description

", + "rob_metric-id": 15, + "rob_metric-name": "overall metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Definitely low risk of bias", + "rob_score-id": 13, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "test", + "rob_score-score": 17, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -175,23 +175,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2020-05-08T13:56:46.183552-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2020-05-08T13:56:46.183552-0400", "rob-final": true, "rob-id": 6, - "rob-last_updated": "2020-05-08T15:34:31.468702-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 1, - "rob-score_description": "Probably low risk of bias", - "rob-score_id": 14, - "rob-score_is_default": true, - "rob-score_label": "test1", - "rob-score_notes": "test", - "rob-score_score": 16, + "rob-last_updated": "2020-05-08T15:34:31.468702-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 1, + "rob_score-description": "Probably low risk of bias", + "rob_score-id": 14, + "rob_score-is_default": true, + "rob_score-label": "test1", + "rob_score-notes": "test", + "rob_score-score": 16, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -218,23 +218,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2020-05-08T13:56:46.183552-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2020-05-08T13:56:46.183552-0400", "rob-final": true, "rob-id": 6, - "rob-last_updated": "2020-05-08T15:34:31.468702-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 1, - "rob-score_description": "Definitely high risk of bias", - "rob-score_id": 16, - "rob-score_is_default": false, - "rob-score_label": "test2", - "rob-score_notes": "beep", - "rob-score_score": 14, + "rob-last_updated": "2020-05-08T15:34:31.468702-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 11, + "rob_domain-name": "overall", + "rob_metric-description": "

overall description

", + "rob_metric-id": 15, + "rob_metric-name": "overall metric 1", + "rob_score-bias_direction": 1, + "rob_score-description": "Probably low risk of bias", + "rob_score-id": 15, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "test", + "rob_score-score": 16, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -261,23 +261,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2020-05-08T13:56:46.183552-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 11, - "rob-domain_name": "overall", + "rob-created": "2020-05-08T13:56:46.183552-0400", "rob-final": true, "rob-id": 6, - "rob-last_updated": "2020-05-08T15:34:31.468702-04:00", - "rob-metric_description": "

overall description

", - "rob-metric_id": 15, - "rob-metric_name": "overall metric 1", - "rob-score_bias_direction": 1, - "rob-score_description": "Probably low risk of bias", - "rob-score_id": 15, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "test", - "rob-score_score": 16, + "rob-last_updated": "2020-05-08T15:34:31.468702-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 1, + "rob_score-description": "Definitely high risk of bias", + "rob_score-id": 16, + "rob_score-is_default": false, + "rob_score-label": "test2", + "rob_score-notes": "beep", + "rob_score-score": 14, "study-ask_author": "not really (example)", "study-bioassay": true, "study-coi_details": "J.B., H.S., J.A., S.J., M.H. and T.S. are employed by specialty chemical manufacturers whose product lines include brominated flame retardants. M.B., N.M., A.R., D.W.S. and D.G.S. are employed by WIL Research Laboratories, a contract research organization commissioned to conduct the guideline DNT study presented herein. L.F. is employed with BioSTAT Consultants and was commissioned to design and evaluate the statistical aspects of the DNT study. The views and opinions expressed in this article are those of the authors and not necessarily those of their respective employers.", @@ -304,23 +304,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2021-09-02T11:57:26.492990-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2021-09-02T11:57:26.492990-0400", "rob-final": false, "rob-id": 7, - "rob-last_updated": "2021-09-02T11:57:26.733132-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Not reported", - "rob-score_id": 17, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "", - "rob-score_score": 12, + "rob-last_updated": "2021-09-02T11:57:26.733132-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Not reported", + "rob_score-id": 17, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "", + "rob_score-score": 12, "study-ask_author": "", "study-bioassay": false, "study-coi_details": "No COI", @@ -347,23 +347,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2021-09-02T11:57:26.492990-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 11, - "rob-domain_name": "overall", + "rob-created": "2021-09-02T11:57:26.492990-0400", "rob-final": false, "rob-id": 7, - "rob-last_updated": "2021-09-02T11:57:26.733132-04:00", - "rob-metric_description": "

overall description

", - "rob-metric_id": 15, - "rob-metric_name": "overall metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Not reported", - "rob-score_id": 18, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "", - "rob-score_score": 12, + "rob-last_updated": "2021-09-02T11:57:26.733132-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 11, + "rob_domain-name": "overall", + "rob_metric-description": "

overall description

", + "rob_metric-id": 15, + "rob_metric-name": "overall metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Not reported", + "rob_score-id": 18, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "", + "rob_score-score": 12, "study-ask_author": "", "study-bioassay": false, "study-coi_details": "No COI", @@ -390,23 +390,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2021-09-02T11:57:27.448945-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2021-09-02T11:57:27.448945-0400", "rob-final": false, "rob-id": 8, - "rob-last_updated": "2021-09-02T11:57:27.851056-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Not reported", - "rob-score_id": 19, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "", - "rob-score_score": 12, + "rob-last_updated": "2021-09-02T11:57:27.851056-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Not reported", + "rob_score-id": 19, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "", + "rob_score-score": 12, "study-ask_author": "", "study-bioassay": false, "study-coi_details": "", @@ -433,23 +433,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2021-09-02T11:57:27.448945-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 11, - "rob-domain_name": "overall", + "rob-created": "2021-09-02T11:57:27.448945-0400", "rob-final": false, "rob-id": 8, - "rob-last_updated": "2021-09-02T11:57:27.851056-04:00", - "rob-metric_description": "

overall description

", - "rob-metric_id": 15, - "rob-metric_name": "overall metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Not reported", - "rob-score_id": 20, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "", - "rob-score_score": 12, + "rob-last_updated": "2021-09-02T11:57:27.851056-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 11, + "rob_domain-name": "overall", + "rob_metric-description": "

overall description

", + "rob_metric-id": 15, + "rob_metric-name": "overall metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Not reported", + "rob_score-id": 20, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "", + "rob_score-score": 12, "study-ask_author": "", "study-bioassay": false, "study-coi_details": "", @@ -476,23 +476,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2021-09-02T11:57:28.629575-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 10, - "rob-domain_name": "domain 1", + "rob-created": "2021-09-02T11:57:28.629575-0400", "rob-final": false, "rob-id": 9, - "rob-last_updated": "2021-09-02T11:57:29.033255-04:00", - "rob-metric_description": "

description

", - "rob-metric_id": 14, - "rob-metric_name": "metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Not reported", - "rob-score_id": 21, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "", - "rob-score_score": 12, + "rob-last_updated": "2021-09-02T11:57:29.033255-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 10, + "rob_domain-name": "domain 1", + "rob_metric-description": "

description

", + "rob_metric-id": 14, + "rob_metric-name": "metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Not reported", + "rob_score-id": 21, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "", + "rob_score-score": 12, "study-ask_author": "", "study-bioassay": false, "study-coi_details": "", @@ -519,23 +519,23 @@ "rob-active": true, "rob-author_id": 3, "rob-author_name": "Team Member", - "rob-created": "2021-09-02T11:57:28.629575-04:00", - "rob-domain_description": "

description

", - "rob-domain_id": 11, - "rob-domain_name": "overall", + "rob-created": "2021-09-02T11:57:28.629575-0400", "rob-final": false, "rob-id": 9, - "rob-last_updated": "2021-09-02T11:57:29.033255-04:00", - "rob-metric_description": "

overall description

", - "rob-metric_id": 15, - "rob-metric_name": "overall metric 1", - "rob-score_bias_direction": 0, - "rob-score_description": "Not reported", - "rob-score_id": 22, - "rob-score_is_default": true, - "rob-score_label": "", - "rob-score_notes": "", - "rob-score_score": 12, + "rob-last_updated": "2021-09-02T11:57:29.033255-0400", + "rob_domain-description": "

description

", + "rob_domain-id": 11, + "rob_domain-name": "overall", + "rob_metric-description": "

overall description

", + "rob_metric-id": 15, + "rob_metric-name": "overall metric 1", + "rob_score-bias_direction": 0, + "rob_score-description": "Not reported", + "rob_score-id": 22, + "rob_score-is_default": true, + "rob_score-label": "", + "rob_score-notes": "", + "rob_score-score": 12, "study-ask_author": "", "study-bioassay": false, "study-coi_details": "", diff --git a/tests/hawc/apps/riskofbias/test_exports.py b/tests/hawc/apps/riskofbias/test_exports.py index 28218c5717..1a71e45d48 100644 --- a/tests/hawc/apps/riskofbias/test_exports.py +++ b/tests/hawc/apps/riskofbias/test_exports.py @@ -3,7 +3,7 @@ from hawc.apps.common.helper import FlatExport from hawc.apps.riskofbias import exports -from hawc.apps.riskofbias.models import RiskOfBias +from hawc.apps.riskofbias.models import RiskOfBiasScore def check_metadata_accuracy(export: FlatExport): @@ -16,16 +16,14 @@ def check_metadata_accuracy(export: FlatExport): @pytest.mark.django_db class TestRiskOfBiasFlat: def test_metadata(self): - qs = RiskOfBias.objects.none() - exporter = exports.RiskOfBiasFlat(qs, filename="test", assessment_id=1) - export = exporter.build_export() + qs = RiskOfBiasScore.objects.none() + export = exports.RiskOfBiasExporter.flat_export(qs, filename="test") check_metadata_accuracy(export) @pytest.mark.django_db class TestRiskOfBiasCompleteFlat: def test_metadata(self): - qs = RiskOfBias.objects.none() - exporter = exports.RiskOfBiasCompleteFlat(qs, filename="test", assessment_id=1) - export = exporter.build_export() + qs = RiskOfBiasScore.objects.none() + export = exports.RiskOfBiasCompleteExporter.flat_export(qs, filename="test") check_metadata_accuracy(export) From bf9afe98547db3d07cc49d780c0f0b67a940f50e Mon Sep 17 00:00:00 2001 From: Daniel Rabstejnek Date: Thu, 12 Oct 2023 21:07:42 -0400 Subject: [PATCH 03/11] Epimeta export rewrite (#922) * preliminary epi rewrite * changes * changes * changes * fix * added back something accidentally deleted * moved code * fix test * remove old stuff * cleanup: * epimeta export rewrite * remove obsolete code * changes * merge fix * update admin site to browse data pivot by evidence type --------- Co-authored-by: Andy Shapiro --- hawc/apps/epimeta/exports.py | 275 ++++++++++++++++++++++++----------- hawc/apps/epimeta/models.py | 120 --------------- hawc/apps/summary/admin.py | 11 +- 3 files changed, 196 insertions(+), 210 deletions(-) diff --git a/hawc/apps/epimeta/exports.py b/hawc/apps/epimeta/exports.py index 1c29ede1b2..426a17b1d4 100644 --- a/hawc/apps/epimeta/exports.py +++ b/hawc/apps/epimeta/exports.py @@ -1,6 +1,158 @@ +import pandas as pd + +from ..common.exports import Exporter, ModelExport from ..common.helper import FlatFileExporter -from ..study.models import Study -from . import models +from ..common.models import sql_display, sql_format, str_m2m +from ..epi.exports import ResultMetricExport +from ..study.exports import StudyExport +from . import constants + + +class MetaProtocolExport(ModelExport): + def get_value_map(self): + return { + "pk": "pk", + "url": "url", + "name": "name", + "protocol_type": "protocol_type", + "lit_search_strategy": "lit_search_strategy", + "lit_search_notes": "lit_search_notes", + "lit_search_start_date": "lit_search_start_date", + "lit_search_end_date": "lit_search_end_date", + "total_references": "total_references", + "inclusion_criteria": "inclusion_criteria", + "exclusion_criteria": "exclusion_criteria", + "total_studies_identified": "total_studies_identified", + "notes": "notes", + } + + def get_annotation_map(self, query_prefix): + return { + "url": sql_format("/epi-meta/protocol/{}/", query_prefix + "id"), # hardcoded URL + "protocol_type": sql_display(query_prefix + "protocol_type", constants.MetaProtocol), + "lit_search_strategy": sql_display( + query_prefix + "lit_search_strategy", constants.MetaLitSearch + ), + "inclusion_criteria": str_m2m(query_prefix + "inclusion_criteria__description"), + "exclusion_criteria": str_m2m(query_prefix + "exclusion_criteria__description"), + } + + def prepare_df(self, df): + for key in [ + self.get_column_name("lit_search_start_date"), + self.get_column_name("lit_search_end_date"), + ]: + if key in df.columns: + df.loc[:, key] = df[key].apply(lambda x: x.isoformat() if not pd.isna(x) else x) + return df + + +class MetaResultExport(ModelExport): + def get_value_map(self): + return { + "pk": "pk", + "url": "url", + "label": "label", + "data_location": "data_location", + "health_outcome": "health_outcome", + "health_outcome_notes": "health_outcome_notes", + "exposure_name": "exposure_name", + "exposure_details": "exposure_details", + "number_studies": "number_studies", + "statistical_metric": "metric__metric", + "statistical_notes": "statistical_notes", + "n": "n", + "estimate": "estimate", + "lower_ci": "lower_ci", + "upper_ci": "upper_ci", + "ci_units": "ci_units", + "heterogeneity": "heterogeneity", + "adjustment_factors": "adjustment_factors_str", + "notes": "notes", + } + + def get_annotation_map(self, query_prefix): + return { + "url": sql_format("/epi-meta/result/{}/", query_prefix + "id"), # hardcoded URL + "adjustment_factors_str": str_m2m(query_prefix + "adjustment_factors__description"), + } + + +class SingleResultExport(ModelExport): + def get_value_map(self): + return { + "pk": "pk", + "study": "study_id", + "exposure_name": "exposure_name", + "weight": "weight", + "n": "n", + "estimate": "estimate", + "lower_ci": "lower_ci", + "upper_ci": "upper_ci", + "ci_units": "ci_units", + "notes": "notes", + } + + +class EpiMetaExporter(Exporter): + def build_modules(self) -> list[ModelExport]: + return [ + StudyExport("study", "protocol__study"), + MetaProtocolExport("meta_protocol", "protocol"), + MetaResultExport("meta_result", ""), + SingleResultExport("single_result", "single_results"), + ] + + +class EpiMetaDataPivotExporter(Exporter): + def build_modules(self) -> list[ModelExport]: + return [ + StudyExport( + "study", + "protocol__study", + include=( + "id", + "short_citation", + "published", + ), + ), + MetaProtocolExport( + "meta_protocol", + "protocol", + include=( + "pk", + "name", + "protocol_type", + "total_references", + "total_studies_identified", + ), + ), + MetaResultExport( + "meta_result", + "", + include=( + "pk", + "label", + "health_outcome", + "exposure_name", + "number_studies", + "n", + "estimate", + "lower_ci", + "upper_ci", + "ci_units", + "heterogeneity", + ), + ), + ResultMetricExport( + "metric", + "metric", + include=( + "name", + "abbreviation", + ), + ), + ] class MetaResultFlatComplete(FlatFileExporter): @@ -9,36 +161,8 @@ class MetaResultFlatComplete(FlatFileExporter): epidemiological meta-result study type from scratch. """ - def _get_header_row(self): - header = [] - header.extend(Study.flat_complete_header_row()) - header.extend(models.MetaProtocol.flat_complete_header_row()) - header.extend(models.MetaResult.flat_complete_header_row()) - header.extend(models.SingleResult.flat_complete_header_row()) - return header - - def _get_data_rows(self): - rows = [] - identifiers_df = Study.identifiers_df(self.queryset, "protocol__study_id") - for obj in self.queryset: - ser = obj.get_json(json_encode=False) - row = [] - row.extend(Study.flat_complete_data_row(ser["protocol"]["study"], identifiers_df)) - row.extend(models.MetaProtocol.flat_complete_data_row(ser["protocol"])) - row.extend(models.MetaResult.flat_complete_data_row(ser)) - - if len(ser["single_results"]) == 0: - # print one-row with no single-results - row.extend([None] * 10) - rows.append(row) - else: - # print each single-result as a new row - for sr in ser["single_results"]: - row_copy = list(row) # clone - row_copy.extend(models.SingleResult.flat_complete_data_row(sr)) - rows.append(row_copy) - - return rows + def build_df(self) -> pd.DataFrame: + return EpiMetaExporter().get_df(self.queryset) class MetaResultFlatDataPivot(FlatFileExporter): @@ -49,60 +173,35 @@ class MetaResultFlatDataPivot(FlatFileExporter): Note: data pivot does not currently include study confidence. Could be added if needed. """ - def _get_header_row(self): - return [ - "study id", - "study name", - "study published", - "protocol id", - "protocol name", - "protocol type", - "total references", - "identified references", - "key", - "meta result id", - "meta result label", - "health outcome", - "exposure", - "result references", - "statistical metric", - "statistical metric abbreviation", - "N", - "estimate", - "lower CI", - "upper CI", - "CI units", - "heterogeneity", - ] + def build_df(self) -> pd.DataFrame: + df = EpiMetaDataPivotExporter().get_df(self.queryset) + + df["key"] = df["meta_result-pk"] - def _get_data_rows(self): - rows = [] - for obj in self.queryset: - ser = obj.get_json(json_encode=False) - row = [ - ser["protocol"]["study"]["id"], - ser["protocol"]["study"]["short_citation"], - ser["protocol"]["study"]["published"], - ser["protocol"]["id"], - ser["protocol"]["name"], - ser["protocol"]["protocol_type"], - ser["protocol"]["total_references"], - ser["protocol"]["total_studies_identified"], - ser["id"], # repeat for data-pivot key - ser["id"], - ser["label"], - ser["health_outcome"], - ser["exposure_name"], - ser["number_studies"], - ser["metric"]["metric"], - ser["metric"]["abbreviation"], - ser["n"], - ser["estimate"], - ser["lower_ci"], - ser["upper_ci"], - ser["ci_units"], - ser["heterogeneity"], - ] - rows.append(row) - - return rows + df = df.rename( + columns={ + "study-id": "study id", + "study-short_citation": "study name", + "study-published": "study published", + "meta_protocol-pk": "protocol id", + "meta_protocol-name": "protocol name", + "meta_protocol-protocol_type": "protocol type", + "meta_protocol-total_references": "total references", + "meta_protocol-total_studies_identified": "identified references", + "meta_result-pk": "meta result id", + "meta_result-label": "meta result label", + "meta_result-health_outcome": "health outcome", + "meta_result-exposure_name": "exposure", + "meta_result-number_studies": "result references", + "metric-name": "statistical metric", + "metric-abbreviation": "statistical metric abbreviation", + "meta_result-n": "N", + "meta_result-estimate": "estimate", + "meta_result-lower_ci": "lower CI", + "meta_result-upper_ci": "upper CI", + "meta_result-ci_units": "CI units", + "meta_result-heterogeneity": "heterogeneity", + }, + errors="raise", + ) + return df diff --git a/hawc/apps/epimeta/models.py b/hawc/apps/epimeta/models.py index 16b1c2c7e7..f9951f0e8b 100644 --- a/hawc/apps/epimeta/models.py +++ b/hawc/apps/epimeta/models.py @@ -69,42 +69,6 @@ def get_absolute_url(self): def get_json(self, json_encode=True): return SerializerHelper.get_serialized(self, json=json_encode, from_cache=False) - @staticmethod - def flat_complete_header_row(): - return ( - "meta_protocol-pk", - "meta_protocol-url", - "meta_protocol-name", - "meta_protocol-protocol_type", - "meta_protocol-lit_search_strategy", - "meta_protocol-lit_search_notes", - "meta_protocol-lit_search_start_date", - "meta_protocol-lit_search_end_date", - "meta_protocol-total_references", - "meta_protocol-inclusion_criteria", - "meta_protocol-exclusion_criteria", - "meta_protocol-total_studies_identified", - "meta_protocol-notes", - ) - - @staticmethod - def flat_complete_data_row(ser): - return ( - ser["id"], - ser["url"], - ser["name"], - ser["protocol_type"], - ser["lit_search_strategy"], - ser["lit_search_notes"], - ser["lit_search_start_date"], - ser["lit_search_end_date"], - ser["total_references"], - "|".join(ser["inclusion_criteria"]), - "|".join(ser["exclusion_criteria"]), - ser["total_studies_identified"], - ser["notes"], - ) - def get_study(self): return self.study @@ -191,54 +155,6 @@ def get_qs_json(queryset, json_encode=True): else: return results - @staticmethod - def flat_complete_header_row(): - return ( - "meta_result-pk", - "meta_result-url", - "meta_result-label", - "meta_result-data_location", - "meta_result-health_outcome", - "meta_result-health_outcome_notes", - "meta_result-exposure_name", - "meta_result-exposure_details", - "meta_result-number_studies", - "meta_result-statistical_metric", - "meta_result-statistical_notes", - "meta_result-n", - "meta_result-estimate", - "meta_result-lower_ci", - "meta_result-upper_ci", - "meta_result-ci_units", - "meta_result-heterogeneity", - "meta_result-adjustment_factors", - "meta_result-notes", - ) - - @staticmethod - def flat_complete_data_row(ser): - return ( - ser["id"], - ser["url"], - ser["label"], - ser["data_location"], - ser["health_outcome"], - ser["health_outcome_notes"], - ser["exposure_name"], - ser["exposure_details"], - ser["number_studies"], - ser["metric"]["metric"], - ser["statistical_notes"], - ser["n"], - ser["estimate"], - ser["lower_ci"], - ser["upper_ci"], - ser["ci_units"], - ser["heterogeneity"], - "|".join(ser["adjustment_factors"]), - ser["notes"], - ) - def get_study(self): if self.protocol is not None: return self.protocol.get_study() @@ -317,42 +233,6 @@ def estimate_formatted(self): txt += f" ({self.lower_ci}, {self.upper_ci})" return txt - @staticmethod - def flat_complete_header_row(): - return ( - "single_result-pk", - "single_result-study", - "single_result-exposure_name", - "single_result-weight", - "single_result-n", - "single_result-estimate", - "single_result-lower_ci", - "single_result-upper_ci", - "single_result-ci_units", - "single_result-notes", - ) - - @staticmethod - def flat_complete_data_row(ser): - study = None - try: - study = ser["study"]["id"] - except TypeError: - pass - - return ( - ser["id"], - study, - ser["exposure_name"], - ser["weight"], - ser["n"], - ser["estimate"], - ser["lower_ci"], - ser["upper_ci"], - ser["ci_units"], - ser["notes"], - ) - def get_study(self): if self.meta_result is not None: return self.meta_result.get_study() diff --git a/hawc/apps/summary/admin.py b/hawc/apps/summary/admin.py index a384ffece6..0f8ecfd38d 100644 --- a/hawc/apps/summary/admin.py +++ b/hawc/apps/summary/admin.py @@ -29,7 +29,6 @@ def show_url(self, obj): return format_html(f"{obj.id}") -@admin.register(models.DataPivotUpload, models.DataPivotQuery) class DataPivotAdmin(admin.ModelAdmin): list_display = ( "title", @@ -40,7 +39,7 @@ class DataPivotAdmin(admin.ModelAdmin): "created", "last_updated", ) - list_filter = ("published", ("assessment", admin.RelatedOnlyFieldListFilter)) + list_filter = ("published", ("evidence_type", admin.RelatedOnlyFieldListFilter)) search_fields = ("assessment__name", "title") @admin.display(description="URL") @@ -48,6 +47,10 @@ def show_url(self, obj): return format_html(f"{obj.id}") +class DataPivotQueryAdmin(DataPivotAdmin): + list_filter = ("published", "evidence_type") + + @admin.register(models.SummaryText) class SummaryTextAdmin(TreeAdmin): list_display = ( @@ -69,3 +72,7 @@ class SummaryTableAdmin(VersionAdmin): ) list_filter = ("table_type", "published", ("assessment", admin.RelatedOnlyFieldListFilter)) + + +admin.site.register(models.DataPivotUpload, DataPivotAdmin) +admin.site.register(models.DataPivotQuery, DataPivotQueryAdmin) From c6003ad9c1124cb93aa59e1ff182867a1b6cc7db Mon Sep 17 00:00:00 2001 From: Danny Peterson Date: Mon, 27 Nov 2023 16:10:42 -0500 Subject: [PATCH 04/11] started simple assessment value exports --- hawc/apps/assessment/exports.py | 29 +++++++++++++++++++++++++++++ hawc/apps/hawc_admin/api.py | 20 ++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/hawc/apps/assessment/exports.py b/hawc/apps/assessment/exports.py index fb40647421..3f1405a9dd 100644 --- a/hawc/apps/assessment/exports.py +++ b/hawc/apps/assessment/exports.py @@ -1,5 +1,6 @@ import pandas as pd +from ..common.exports import Exporter, ModelExport from ..common.helper import FlatFileExporter from .models import AssessmentValue @@ -7,3 +8,31 @@ class ValuesListExport(FlatFileExporter): def build_df(self) -> pd.DataFrame: return AssessmentValue.objects.get_df() + + +class AssessmentValueExport(ModelExport): + def get_value_map(self) -> dict: + return { + "pk": "pk", + "evaluation_type": "evaluation_type", + "system": "system", + } + + +class AssessmentExport(ModelExport): + def get_value_map(self) -> dict: + return {"id": "id", "name": "name"} + + +class AssessmentDetail(ModelExport): + def get_value_map(self) -> dict: + return {"id": "id"} + + +class AssessmentExporter(Exporter): + def build_modules(self) -> list[ModelExport]: + return [ + AssessmentValueExport("assessment_value", ""), + AssessmentExport("assessment", "assessment"), + # AssessmentExport("assessment_detail", ""), + ] diff --git a/hawc/apps/hawc_admin/api.py b/hawc/apps/hawc_admin/api.py index 5bb5cd64a4..a4a1fb8d44 100644 --- a/hawc/apps/hawc_admin/api.py +++ b/hawc/apps/hawc_admin/api.py @@ -4,6 +4,7 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response +from ..assessment import exports from ..assessment.exports import ValuesListExport from ..assessment.models import AssessmentValue from ..common.api import FivePerMinuteThrottle @@ -40,7 +41,18 @@ class ReportsViewSet(viewsets.ViewSet): @action(detail=False, renderer_classes=PandasRenderers) def values(self, request): """Gets all value data across all assessments.""" - export = ValuesListExport( - queryset=AssessmentValue.objects.all(), filename="hawc-assessment-values" - ).build_export() - return Response(export, status=status.HTTP_200_OK) + qs = AssessmentValue.objects.all().select_related("assessment", "assessment_detail") + exporter = exports.AssessmentExporter.flat_export(qs, filename="hawc-assessment-values") + # export = ValuesListExport( + # queryset=AssessmentValue.objects.all().select_related("assessment", "assessment_id"), + # filename="hawc-assessment-values", + # ).build_export() + return Response(exporter, status=status.HTTP_200_OK) + + # @action(detail=False, renderer_classes=PandasRenderers) + # def values(self, request): + # """Gets all value data across all assessments.""" + # export = ValuesListExport( + # queryset=AssessmentValue.objects.all(), filename="hawc-assessment-values" + # ).build_export() + # return Response(export, status=status.HTTP_200_OK) From dd47a5c7678a4358ee975caafd172312ac7fc385 Mon Sep 17 00:00:00 2001 From: Danny Peterson Date: Tue, 28 Nov 2023 14:54:46 -0500 Subject: [PATCH 05/11] added more assessment fields --- hawc/apps/assessment/exports.py | 57 +++++++++++++++++++++++++++++---- hawc/apps/hawc_admin/api.py | 16 ++------- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/hawc/apps/assessment/exports.py b/hawc/apps/assessment/exports.py index 3f1405a9dd..88d4376fbf 100644 --- a/hawc/apps/assessment/exports.py +++ b/hawc/apps/assessment/exports.py @@ -2,6 +2,9 @@ from ..common.exports import Exporter, ModelExport from ..common.helper import FlatFileExporter +from ..common.models import sql_display, sql_format, str_m2m +from ..study.exports import StudyExport +from . import constants from .models import AssessmentValue @@ -13,26 +16,66 @@ def build_df(self) -> pd.DataFrame: class AssessmentValueExport(ModelExport): def get_value_map(self) -> dict: return { - "pk": "pk", - "evaluation_type": "evaluation_type", + "evaluation_type": "evaluation_type_display", "system": "system", + "value_type": "value_type_display", + "value": "value", + "value_unit": "value_unit", + "confidence": "confidence", + "pod_value": "pod_value", + "pod_unit": "pod_unit", + "tumor_type": "tumor_type", + "extrapolation_method": "extrapolation_method", + "comments": "comments", + "extra": "extra", + } + + def get_annotation_map(self, query_prefix: str) -> dict: + return { + "evaluation_type_display": sql_display( + query_prefix + "evaluation_type", constants.EvaluationType + ), + "value_type_display": sql_display(query_prefix + "value_type", constants.ValueType), } class AssessmentExport(ModelExport): def get_value_map(self) -> dict: - return {"id": "id", "name": "name"} + return {"id": "id", "name": "name", "created": "created", "last_updated": "last_updated"} -class AssessmentDetail(ModelExport): +class AssessmentDetailExport(ModelExport): def get_value_map(self) -> dict: - return {"id": "id"} + return { + "project_type": "project_type", + "project_status": "project_status_display", + "project_url": "project_url", + "peer_review_status": "peer_review_status", + "qa_id": "qa_id", + "qa_url": "qa_url", + "report_id": "report_id", + "report_url": "report_url", + } + + def get_annotation_map(self, query_prefix: str) -> dict: + return { + "project_status_display": sql_display(query_prefix + "project_status", constants.Status) + } class AssessmentExporter(Exporter): def build_modules(self) -> list[ModelExport]: return [ - AssessmentValueExport("assessment_value", ""), AssessmentExport("assessment", "assessment"), - # AssessmentExport("assessment_detail", ""), + AssessmentDetailExport("assessment_detail", "assessment__details"), + AssessmentValueExport("assessment_value", ""), + StudyExport( + "study", + "study", + include=( + "id", + "short_citation", + "published", + ), + ), ] diff --git a/hawc/apps/hawc_admin/api.py b/hawc/apps/hawc_admin/api.py index a4a1fb8d44..4ffb6f1317 100644 --- a/hawc/apps/hawc_admin/api.py +++ b/hawc/apps/hawc_admin/api.py @@ -41,18 +41,6 @@ class ReportsViewSet(viewsets.ViewSet): @action(detail=False, renderer_classes=PandasRenderers) def values(self, request): """Gets all value data across all assessments.""" - qs = AssessmentValue.objects.all().select_related("assessment", "assessment_detail") + qs = AssessmentValue.objects.all() exporter = exports.AssessmentExporter.flat_export(qs, filename="hawc-assessment-values") - # export = ValuesListExport( - # queryset=AssessmentValue.objects.all().select_related("assessment", "assessment_id"), - # filename="hawc-assessment-values", - # ).build_export() - return Response(exporter, status=status.HTTP_200_OK) - - # @action(detail=False, renderer_classes=PandasRenderers) - # def values(self, request): - # """Gets all value data across all assessments.""" - # export = ValuesListExport( - # queryset=AssessmentValue.objects.all(), filename="hawc-assessment-values" - # ).build_export() - # return Response(export, status=status.HTTP_200_OK) + return Response(exporter) From 513bc529bbb7a663849632835027cfbb7c422d65 Mon Sep 17 00:00:00 2001 From: Andy Shapiro Date: Tue, 28 Nov 2023 14:58:18 -0500 Subject: [PATCH 06/11] add helper method to filter querysets in custom api viewset actions --- hawc/apps/animal/api.py | 5 ++++- hawc/apps/assessment/filterset.py | 21 +++++++++++++++++++++ hawc/apps/common/api/filters.py | 25 +++++++++++++++++++++++++ hawc/apps/hawc_admin/api.py | 6 +++++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/hawc/apps/animal/api.py b/hawc/apps/animal/api.py index 023d80d862..ceedf2ac6b 100644 --- a/hawc/apps/animal/api.py +++ b/hawc/apps/animal/api.py @@ -13,6 +13,7 @@ DoseUnitsViewSet, ) from ..assessment.constants import AssessmentViewSetPermissions +from ..common.api.filters import filtered_qs from ..common.helper import FlatExport, cacheable from ..common.renderers import PandasRenderers from ..common.serializers import HeatmapQuerySerializer, UnusedSerializer @@ -20,6 +21,7 @@ from . import exports, models, serializers from .actions.model_metadata import AnimalMetadata from .actions.term_check import term_check +from .filterset import EndpointFilterSet class AnimalAssessmentViewSet(BaseAssessmentViewSet): @@ -43,8 +45,9 @@ def full_export(self, request, pk): Retrieve complete animal data """ self.assessment = self.get_object() + qs = self.get_endpoint_queryset() exporter = exports.EndpointGroupFlatComplete( - self.get_endpoint_queryset(), + filtered_qs(qs, EndpointFilterSet, request, assessment=self.assessment), filename=f"{self.assessment}-bioassay-complete", assessment=self.assessment, ) diff --git a/hawc/apps/assessment/filterset.py b/hawc/apps/assessment/filterset.py index c5e9db3e00..b3540f5454 100644 --- a/hawc/apps/assessment/filterset.py +++ b/hawc/apps/assessment/filterset.py @@ -165,3 +165,24 @@ def create_form(self): class EffectTagFilterSet(df.FilterSet): name = df.CharFilter(lookup_expr="icontains") + + +class AssessmentValueFilterSet(df.FilterSet): + name = df.CharFilter( + field_name="assessment__name", + lookup_expr="icontains", + label="Assessment name", + ) + cas = df.CharFilter( + field_name="assessment__cas", + label="Assessment CAS", + ) + project_type = df.CharFilter( + field_name="assessment__details__project_type", + lookup_expr="icontains", + label="Assessment project type", + ) + + class Meta: + model = models.AssessmentValue + fields = ["value_type"] diff --git a/hawc/apps/common/api/filters.py b/hawc/apps/common/api/filters.py index ceace5d1d7..9256c72368 100644 --- a/hawc/apps/common/api/filters.py +++ b/hawc/apps/common/api/filters.py @@ -1,4 +1,8 @@ +from django.db.models import QuerySet +from django_filters.filterset import FilterSet +from django_filters.utils import translate_validation from rest_framework import exceptions, filters +from rest_framework.request import Request from ..helper import try_parse_list_ints @@ -30,3 +34,24 @@ def filter_queryset(self, request, queryset, view): raise exceptions.PermissionDenied() return queryset + + +def filtered_qs( + queryset: QuerySet, filterset_cls: type[FilterSet], request: Request, **kw +) -> QuerySet: + """ + Filter a queryset based on a FilterSet and a DRF request. + + This is a utility method to add filtering to custom ViewSet actions, it is adapted from the + `django_filters.rest_framework.FilterSet` used for standard ViewSet operations. + + Args: + queryset (QuerySet): the queryset to filter + filterset_cls (type[filters.FilterSet]): a FilterSet class + request (Request): a rest framework request + **kw: any additional arguments provided to FilterSet class constructor + """ + filterset = filterset_cls(data=request.query_params, queryset=queryset, request=request, **kw) + if not filterset.is_valid(): + raise translate_validation(filterset.errors) + return filterset.qs diff --git a/hawc/apps/hawc_admin/api.py b/hawc/apps/hawc_admin/api.py index 4ffb6f1317..cb23b6a275 100644 --- a/hawc/apps/hawc_admin/api.py +++ b/hawc/apps/hawc_admin/api.py @@ -6,8 +6,10 @@ from ..assessment import exports from ..assessment.exports import ValuesListExport +from ..assessment.filterset import AssessmentValueFilterSet from ..assessment.models import AssessmentValue from ..common.api import FivePerMinuteThrottle +from ..common.api.filters import filtered_qs from ..common.helper import FlatExport from ..common.renderers import PandasRenderers from .actions import media_metadata_report @@ -42,5 +44,7 @@ class ReportsViewSet(viewsets.ViewSet): def values(self, request): """Gets all value data across all assessments.""" qs = AssessmentValue.objects.all() - exporter = exports.AssessmentExporter.flat_export(qs, filename="hawc-assessment-values") + exporter = exports.AssessmentExporter.flat_export( + filtered_qs(qs, AssessmentValueFilterSet, request), filename="hawc-assessment-values" + ) return Response(exporter) From b26b388e64ae2b0e6d79f268d6b82539bc003aa5 Mon Sep 17 00:00:00 2001 From: Danny Peterson Date: Tue, 28 Nov 2023 16:26:40 -0500 Subject: [PATCH 07/11] added assessment fields, and year filter --- hawc/apps/assessment/exports.py | 44 ++++++++++++++++++++++--------- hawc/apps/assessment/filterset.py | 1 + hawc/apps/hawc_admin/api.py | 1 - 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/hawc/apps/assessment/exports.py b/hawc/apps/assessment/exports.py index 88d4376fbf..0786743ba6 100644 --- a/hawc/apps/assessment/exports.py +++ b/hawc/apps/assessment/exports.py @@ -1,16 +1,7 @@ -import pandas as pd - from ..common.exports import Exporter, ModelExport -from ..common.helper import FlatFileExporter -from ..common.models import sql_display, sql_format, str_m2m +from ..common.models import sql_display from ..study.exports import StudyExport from . import constants -from .models import AssessmentValue - - -class ValuesListExport(FlatFileExporter): - def build_df(self) -> pd.DataFrame: - return AssessmentValue.objects.get_df() class AssessmentValueExport(ModelExport): @@ -21,13 +12,22 @@ def get_value_map(self) -> dict: "value_type": "value_type_display", "value": "value", "value_unit": "value_unit", + "adaf": "adaf", "confidence": "confidence", + "duration": "duration", + "basis": "basis", + "pod_type": "pod_type", "pod_value": "pod_value", "pod_unit": "pod_unit", + "uncertainty": "uncertainty_display", + "species_studied": "species_studied", + "evidence": "evidence", "tumor_type": "tumor_type", "extrapolation_method": "extrapolation_method", "comments": "comments", "extra": "extra", + "created": "created", + "last_updated": "last_updated", } def get_annotation_map(self, query_prefix: str) -> dict: @@ -36,12 +36,22 @@ def get_annotation_map(self, query_prefix: str) -> dict: query_prefix + "evaluation_type", constants.EvaluationType ), "value_type_display": sql_display(query_prefix + "value_type", constants.ValueType), + "uncertainty_display": sql_display( + query_prefix + "uncertainty", constants.UncertaintyChoices + ), } class AssessmentExport(ModelExport): def get_value_map(self) -> dict: - return {"id": "id", "name": "name", "created": "created", "last_updated": "last_updated"} + return { + "id": "id", + "name": "name", + "cas": "cas", + "year": "year", + "created": "created", + "last_updated": "last_updated", + } class AssessmentDetailExport(ModelExport): @@ -50,16 +60,24 @@ def get_value_map(self) -> dict: "project_type": "project_type", "project_status": "project_status_display", "project_url": "project_url", - "peer_review_status": "peer_review_status", + "peer_review_status": "peer_review_status_display", "qa_id": "qa_id", "qa_url": "qa_url", "report_id": "report_id", "report_url": "report_url", + "extra": "extra", + "created": "created", + "last_updated": "last_updated", } def get_annotation_map(self, query_prefix: str) -> dict: return { - "project_status_display": sql_display(query_prefix + "project_status", constants.Status) + "project_status_display": sql_display( + query_prefix + "project_status", constants.Status + ), + "peer_review_status_display": sql_display( + query_prefix + "peer_review_status", constants.PeerReviewType + ), } diff --git a/hawc/apps/assessment/filterset.py b/hawc/apps/assessment/filterset.py index b3540f5454..6dd1f1135e 100644 --- a/hawc/apps/assessment/filterset.py +++ b/hawc/apps/assessment/filterset.py @@ -182,6 +182,7 @@ class AssessmentValueFilterSet(df.FilterSet): lookup_expr="icontains", label="Assessment project type", ) + year = df.CharFilter(field_name="assessment__year", label="Assessment year") class Meta: model = models.AssessmentValue diff --git a/hawc/apps/hawc_admin/api.py b/hawc/apps/hawc_admin/api.py index cb23b6a275..80f4737127 100644 --- a/hawc/apps/hawc_admin/api.py +++ b/hawc/apps/hawc_admin/api.py @@ -5,7 +5,6 @@ from rest_framework.response import Response from ..assessment import exports -from ..assessment.exports import ValuesListExport from ..assessment.filterset import AssessmentValueFilterSet from ..assessment.models import AssessmentValue from ..common.api import FivePerMinuteThrottle From fb9689a2f0bb653b79caf610ce8402290fdc4a80 Mon Sep 17 00:00:00 2001 From: Danny Peterson Date: Wed, 29 Nov 2023 14:22:34 -0500 Subject: [PATCH 08/11] added order_by and pagination fields to filterset --- hawc/apps/assessment/filterset.py | 15 +++++++++++++++ hawc/apps/hawc_admin/api.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/hawc/apps/assessment/filterset.py b/hawc/apps/assessment/filterset.py index 6dd1f1135e..92e38ca0ea 100644 --- a/hawc/apps/assessment/filterset.py +++ b/hawc/apps/assessment/filterset.py @@ -8,6 +8,8 @@ BaseFilterSet, ExpandableFilterForm, InlineFilterForm, + OrderingFilter, + PaginationFilter, ) from ..common.helper import new_window_a from ..myuser.models import HAWCUser @@ -184,6 +186,19 @@ class AssessmentValueFilterSet(df.FilterSet): ) year = df.CharFilter(field_name="assessment__year", label="Assessment year") + order_by = OrderingFilter( + fields=( + ( + "assessment__name", + "name", + ), + ("assessment__id", "assessment_id"), + ), + initial="name", + ) + + paginate_by = PaginationFilter() + class Meta: model = models.AssessmentValue fields = ["value_type"] diff --git a/hawc/apps/hawc_admin/api.py b/hawc/apps/hawc_admin/api.py index 80f4737127..d8c6fca3ac 100644 --- a/hawc/apps/hawc_admin/api.py +++ b/hawc/apps/hawc_admin/api.py @@ -1,5 +1,5 @@ from django.utils import timezone -from rest_framework import permissions, status, viewsets +from rest_framework import permissions, viewsets from rest_framework.decorators import action from rest_framework.renderers import JSONRenderer from rest_framework.response import Response From fa07b7748b27a4e90ecf0770c4b6fea2ecc6ef11 Mon Sep 17 00:00:00 2001 From: Danny Peterson Date: Thu, 30 Nov 2023 11:18:45 -0500 Subject: [PATCH 09/11] changed format_time to accomodate pandas update --- hawc/apps/assessment/exports.py | 9 +++++++++ hawc/apps/common/exports.py | 2 +- tests/hawc/apps/hawc_admin/test_api.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/hawc/apps/assessment/exports.py b/hawc/apps/assessment/exports.py index 0786743ba6..6ce4f77a70 100644 --- a/hawc/apps/assessment/exports.py +++ b/hawc/apps/assessment/exports.py @@ -41,6 +41,9 @@ def get_annotation_map(self, query_prefix: str) -> dict: ), } + def prepare_df(self, df): + return self.format_time(df) + class AssessmentExport(ModelExport): def get_value_map(self) -> dict: @@ -53,6 +56,9 @@ def get_value_map(self) -> dict: "last_updated": "last_updated", } + def prepare_df(self, df): + return self.format_time(df) + class AssessmentDetailExport(ModelExport): def get_value_map(self) -> dict: @@ -70,6 +76,9 @@ def get_value_map(self) -> dict: "last_updated": "last_updated", } + def prepare_df(self, df): + return self.format_time(df) + def get_annotation_map(self, query_prefix: str) -> dict: return { "project_status_display": sql_display( diff --git a/hawc/apps/common/exports.py b/hawc/apps/common/exports.py index 94205d5774..9af6d80df6 100644 --- a/hawc/apps/common/exports.py +++ b/hawc/apps/common/exports.py @@ -152,7 +152,7 @@ def format_time(self, df: pd.DataFrame) -> pd.DataFrame: tz = timezone.get_default_timezone() for key in [self.get_column_name("created"), self.get_column_name("last_updated")]: if key in df.columns: - df.loc[:, key] = df[key].dt.tz_convert(tz).dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z") + df[key] = df[key].dt.tz_convert(tz).dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z") return df def get_df(self, qs: QuerySet) -> pd.DataFrame: diff --git a/tests/hawc/apps/hawc_admin/test_api.py b/tests/hawc/apps/hawc_admin/test_api.py index 19b6db3e6c..0621318040 100644 --- a/tests/hawc/apps/hawc_admin/test_api.py +++ b/tests/hawc/apps/hawc_admin/test_api.py @@ -80,4 +80,4 @@ def test_assessment_values(self): resp = client.get(url) assert resp.status_code == 200 df = pd.read_json(StringIO(resp.content.decode())) - assert df.shape == (3, 33) + assert df.shape == (3, 41) From f8c87e60b45c19696f295edede491d9c25d878f5 Mon Sep 17 00:00:00 2001 From: Danny Peterson Date: Fri, 1 Dec 2023 13:31:02 -0500 Subject: [PATCH 10/11] fixed filtering for df with empty values --- hawc/apps/common/exports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hawc/apps/common/exports.py b/hawc/apps/common/exports.py index 9af6d80df6..0ae15eda61 100644 --- a/hawc/apps/common/exports.py +++ b/hawc/apps/common/exports.py @@ -151,7 +151,7 @@ def format_time(self, df: pd.DataFrame) -> pd.DataFrame: return df tz = timezone.get_default_timezone() for key in [self.get_column_name("created"), self.get_column_name("last_updated")]: - if key in df.columns: + if key in df.columns and not df[key].isnull().all(): df[key] = df[key].dt.tz_convert(tz).dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z") return df From 00e2dc7b02d8929aad3842bb6e38dd93c417e16e Mon Sep 17 00:00:00 2001 From: Andy Shapiro Date: Tue, 3 Sep 2024 16:04:06 -0400 Subject: [PATCH 11/11] add a few more options --- hawc/apps/assessment/exports.py | 16 ++++----- hawc/apps/assessment/filterset.py | 14 ++++---- hawc/apps/assessment/managers.py | 50 +------------------------- hawc/apps/hawc_admin/api.py | 2 +- tests/hawc/apps/hawc_admin/test_api.py | 2 +- 5 files changed, 17 insertions(+), 67 deletions(-) diff --git a/hawc/apps/assessment/exports.py b/hawc/apps/assessment/exports.py index 6ce4f77a70..45a4f531d2 100644 --- a/hawc/apps/assessment/exports.py +++ b/hawc/apps/assessment/exports.py @@ -1,5 +1,5 @@ from ..common.exports import Exporter, ModelExport -from ..common.models import sql_display +from ..common.models import sql_display, str_m2m from ..study.exports import StudyExport from . import constants @@ -51,11 +51,17 @@ def get_value_map(self) -> dict: "id": "id", "name": "name", "cas": "cas", + "dtxsids": "dtxsids__name", "year": "year", "created": "created", "last_updated": "last_updated", } + def get_annotation_map(self, query_prefix): + return { + "dtxsids__name": str_m2m(query_prefix + "dtxsids__dtxsid"), + } + def prepare_df(self, df): return self.format_time(df) @@ -97,12 +103,6 @@ def build_modules(self) -> list[ModelExport]: AssessmentDetailExport("assessment_detail", "assessment__details"), AssessmentValueExport("assessment_value", ""), StudyExport( - "study", - "study", - include=( - "id", - "short_citation", - "published", - ), + "study", "study", include=("id", "short_citation", "hero_id", "pubmed_id", "doi") ), ] diff --git a/hawc/apps/assessment/filterset.py b/hawc/apps/assessment/filterset.py index ca8d6b1890..149a597442 100644 --- a/hawc/apps/assessment/filterset.py +++ b/hawc/apps/assessment/filterset.py @@ -179,6 +179,10 @@ class AssessmentValueFilterSet(df.FilterSet): field_name="assessment__cas", label="Assessment CAS", ) + dtxsid = df.CharFilter( + field_name="assessment__dtxsids__dtxsid", + label="Assessment DTXSID", + ) project_type = df.CharFilter( field_name="assessment__details__project_type", lookup_expr="icontains", @@ -187,13 +191,7 @@ class AssessmentValueFilterSet(df.FilterSet): year = df.CharFilter(field_name="assessment__year", label="Assessment year") order_by = OrderingFilter( - fields=( - ( - "assessment__name", - "name", - ), - ("assessment__id", "assessment_id"), - ), + fields=(("assessment__name", "name"), ("assessment__id", "assessment_id")), initial="name", ) @@ -201,4 +199,4 @@ class AssessmentValueFilterSet(df.FilterSet): class Meta: model = models.AssessmentValue - fields = ["value_type"] + fields = ("value_type",) diff --git a/hawc/apps/assessment/managers.py b/hawc/apps/assessment/managers.py index 95125af0f4..00534275f0 100644 --- a/hawc/apps/assessment/managers.py +++ b/hawc/apps/assessment/managers.py @@ -8,7 +8,7 @@ from django.db.models import Case, Exists, OuterRef, Q, QuerySet, Value, When from reversion.models import Version -from ..common.helper import HAWCDjangoJSONEncoder, map_enum +from ..common.helper import HAWCDjangoJSONEncoder from ..common.models import BaseManager, replace_null, str_m2m from . import constants @@ -202,54 +202,6 @@ class DatasetManager(BaseManager): class AssessmentValueManager(BaseManager): assessment_relation = "assessment" - def get_df(self) -> pd.DataFrame: - """Get a dataframe of Assessment Values from given Queryset of Values.""" - mapping: dict[str, str] = { - "assessment_id": "assessment_id", - "assessment__name": "assessment_name", - "assessment__created": "assessment_created", - "assessment__last_updated": "assessment_last_updated", - "assessment__details__project_type": "project_type", - "assessment__details__project_status": "project_status", - "assessment__details__project_url": "project_url", - "assessment__details__peer_review_status": "peer_review_status", - "assessment__details__qa_id": "qa_id", - "assessment__details__qa_url": "qa_url", - "assessment__details__report_id": "report_id", - "assessment__details__report_url": "report_url", - "assessment__details__extra": "assessment_extra", - "evaluation_type": "evaluation_type", - "id": "value_id", - "system": "system", - "value_type": "value_type", - "value": "value", - "value_unit": "value_unit", - "basis": "basis", - "pod_value": "pod_value", - "pod_unit": "pod_unit", - "species_studied": "species_studied", - "duration": "duration", - "study_id": "study_id", - "study__short_citation": "study_citation", - "confidence": "confidence", - "uncertainty": "uncertainty", - "tumor_type": "tumor_type", - "extrapolation_method": "extrapolation_method", - "evidence": "evidence", - "comments": "comments", - "extra": "extra", - } - data = self.select_related("assessment__details").values_list(*list(mapping.keys())) - df = pd.DataFrame(data=data, columns=list(mapping.values())).sort_values( - ["assessment_id", "value_id"] - ) - map_enum(df, "project_status", constants.Status, replace=True) - map_enum(df, "peer_review_status", constants.PeerReviewType, replace=True) - map_enum(df, "evaluation_type", constants.EvaluationType, replace=True) - map_enum(df, "value_type", constants.ValueType, replace=True) - map_enum(df, "confidence", constants.Confidence, replace=True) - return df - class AssessmentDetailManager(BaseManager): assessment_relation = "assessment" diff --git a/hawc/apps/hawc_admin/api.py b/hawc/apps/hawc_admin/api.py index d8c6fca3ac..49fa572440 100644 --- a/hawc/apps/hawc_admin/api.py +++ b/hawc/apps/hawc_admin/api.py @@ -44,6 +44,6 @@ def values(self, request): """Gets all value data across all assessments.""" qs = AssessmentValue.objects.all() exporter = exports.AssessmentExporter.flat_export( - filtered_qs(qs, AssessmentValueFilterSet, request), filename="hawc-assessment-values" + filtered_qs(qs, AssessmentValueFilterSet, request), filename="assessment-values" ) return Response(exporter) diff --git a/tests/hawc/apps/hawc_admin/test_api.py b/tests/hawc/apps/hawc_admin/test_api.py index 998205576a..7e27db0ca6 100644 --- a/tests/hawc/apps/hawc_admin/test_api.py +++ b/tests/hawc/apps/hawc_admin/test_api.py @@ -82,7 +82,7 @@ def test_assessment_values(self): resp = client.get(url) assert resp.status_code == 200 df = pd.read_json(StringIO(resp.content.decode())) - assert df.shape == (3, 41) + assert df.shape == (3, 44) @pytest.mark.django_db