Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visual and Data Pivot - read/write API #896

Merged
merged 19 commits into from
Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions client/hawc_client/summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,74 @@ class SummaryClient(BaseClient):
Client class for summary requests.
"""

def create_visual(self, data: dict) -> dict:
"""Create a new visual

Args:
data (dict): Required metadata for object creation.
- title (str): Visual title
- slug (str): Visual identifier/URL base
- visual_type (int): Constant representing visual type
- published (bool): visual is published for public view
- settings (dict): object settings (must be valid JSON)
- assessment (int): assessment ID
- prefilters (dict): object prefilters (must be valid JSON)
- caption (str): Visual caption
- sort_order (str): how results are sorted

Returns:
dict: The resulting object, if create was successful
"""
url = f"{self.session.root_url}/summary/api/visual/"
return self.session.post(url, data=data).json()

def update_visual(self, visual_id: int, data: dict) -> dict:
"""Create a new visual

Args:
id (int): Visual identifier
data (dict): Metadata to update
- title (str): Visual title
- slug (str): Visual identifier/URL base
- visual_type (int): Constant representing visual type
- published (bool): visual is published for public view
- settings (dict): object settings (must be valid JSON)
- assessment (int): assessment ID
- prefilters (dict): object prefilters (must be valid JSON)
- caption (str): Visual caption
- sort_order (str): how results are sorted

Returns:
dict: The resulting object, if create was successful
"""
url = f"{self.session.root_url}/summary/api/visual/{visual_id}/"
return self.session.patch(url, data=data).json()

def delete_visual(self, visual_id: int):
"""Delete a visual.

Args:
visual_id (int): ID of the visual to delete

Returns:
None: If the operation is successful there is no return value.
If the operation is unsuccessful, an error will be raised.
"""
url = f"{self.session.root_url}/summary/api/visual/{visual_id}/"
self.session.delete(url)

def get_visual(self, visual_id: int):
"""Get a visual.

Args:
visual_id (int): ID of the visual to read

Returns:

"""
url = f"{self.session.root_url}/summary/api/visual/{visual_id}/"
return self.session.get(url)

def visual_list(self, assessment_id: int) -> pd.DataFrame:
"""
Retrieves a visual list for the given assessment.
Expand All @@ -22,6 +90,91 @@ def visual_list(self, assessment_id: int) -> pd.DataFrame:
response_json = self.session.get(url).json()
return pd.DataFrame(response_json)

def create_datapivot(self, data: dict) -> dict:
"""Create a new data pivot (query)

Args:
data (dict): Required metadata for object creation.
- title (str): Visual title
- slug (str): Visual identifier/URL base
- evidence_type (int): Constant representing type of evidence used in data pivot
(see hawc.apps.study.constants.StudyType)
- export_style (int): Constant representing how the level at which data are aggregated,
and therefore which columns and types of data are presented in the export, for use
in the visual (see hawc.apps.summary.constants.ExportStyle)
- preferred_units: List of preferred dose-values IDs, in order of preference.
If empty, dose-units will be random for each endpoint
presented. This setting may used for comparing
percent-response, where dose-units are not needed, or for
creating one plot similar, but not identical, dose-units.
- published (bool): datapivot is published for public view
- settings (str): JSON of object settings
- assessment (int): assessment ID
- prefilters (str): JSON of object prefilters
- caption (str): Data pivot caption

Returns:
dict: The resulting object, if create was successful
"""
url = f"{self.session.root_url}/summary/api/data_pivot_query/"
return self.session.post(url, data=data).json()

def update_datapivot(self, datapivot_id: int, data: dict) -> dict:
"""Update an existing data pivot (query)

Args:
id (int): Data pivot identifier
data (dict): Required metadata for object creation.
- title (str): Visual title
- slug (str): Visual identifier/URL base
- evidence_type (int): Constant representing type of evidence used in data pivot
(see hawc.apps.study.constants.StudyType)
- export_style (int): Constant representing how the level at which data are aggregated,
and therefore which columns and types of data are presented in the export, for use
in the visual (see hawc.apps.summary.constants.ExportStyle)
- preferred_units: List of preferred dose-values IDs, in order of preference.
If empty, dose-units will be random for each endpoint
presented. This setting may used for comparing
percent-response, where dose-units are not needed, or for
creating one plot similar, but not identical, dose-units.
- published (bool): datapivot is published for public view
- settings (str): JSON of object settings
- assessment (int): assessment ID
- prefilters (str): JSON of object prefilters
- caption (str): Data pivot caption

Returns:
dict: The resulting object, if update was successful
"""
url = f"{self.session.root_url}/summary/api/data_pivot_query/{datapivot_id}/"
return self.session.patch(url, data=data).json()

def get_datapivot(self, datapivot_id: int):
"""Get a data pivot (query).

Args:
visual_id (int): ID of the visual to read

Returns:
dict: object, if successful

"""
url = f"{self.session.root_url}/summary/api/data_pivot_query/{datapivot_id}/"
return self.session.get(url)

def delete_datapivot(self, datapivot_id: int):
"""Delete a data pivot (query).

Args:
visual_id (int): ID of the visual to delete

Returns:
None: If the operation is successful there is no return value.
If the operation is unsuccessful, an error will be raised.
"""
url = f"{self.session.root_url}/summary/api/data_pivot_query/{datapivot_id}/"
self.session.delete(url)

def datapivot_list(self, assessment_id: int) -> pd.DataFrame:
"""
Retrieves a data pivot list for the given assessment.
Expand Down
11 changes: 10 additions & 1 deletion hawc/apps/summary/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,23 @@ def data(self, request, pk):
return Response(export)


class VisualViewSet(AssessmentViewSet):
class DataPivotQueryViewSet(EditPermissionsCheckMixin, AssessmentEditViewSet):
edit_check_keys = ["assessment"]
assessment_filter_args = "assessment"
model = models.DataPivotQuery
filter_backends = (InAssessmentFilter, UnpublishedFilter)
serializer_class = serializers.DataPivotQuerySerializer


class VisualViewSet(EditPermissionsCheckMixin, AssessmentEditViewSet):
"""
For list view, return all Visual objects for an assessment, but using the
simplified collection view.

For all other views, use the detailed visual view.
"""

edit_check_keys = ["assessment"]
assessment_filter_args = "assessment"
model = models.Visual
pagination_class = DisabledPagination
Expand Down
43 changes: 19 additions & 24 deletions hawc/apps/summary/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def __init__(self, *args, **kwargs):
self.fields["required_tags"].widget.attrs.update(size=10)
self.fields["pruned_tags"].widget.attrs.update(size=10)

data = json.loads(self.instance.settings)
data = self.instance.settings
if "root_node" in data:
self.fields["root_node"].initial = data["root_node"]
if "required_tags" in data:
Expand All @@ -404,17 +404,15 @@ def __init__(self, *args, **kwargs):
self.fields["show_counts"].initial = data["show_counts"]

def save(self, commit=True):
self.instance.settings = json.dumps(
dict(
root_node=self.cleaned_data["root_node"],
required_tags=self.cleaned_data["required_tags"],
pruned_tags=self.cleaned_data["pruned_tags"],
hide_empty_tag_nodes=self.cleaned_data["hide_empty_tag_nodes"],
width=self.cleaned_data["width"],
height=self.cleaned_data["height"],
show_legend=self.cleaned_data["show_legend"],
show_counts=self.cleaned_data["show_counts"],
)
self.instance.settings = dict(
root_node=self.cleaned_data["root_node"],
required_tags=self.cleaned_data["required_tags"],
pruned_tags=self.cleaned_data["pruned_tags"],
hide_empty_tag_nodes=self.cleaned_data["hide_empty_tag_nodes"],
width=self.cleaned_data["width"],
height=self.cleaned_data["height"],
show_legend=self.cleaned_data["show_legend"],
show_counts=self.cleaned_data["show_counts"],
)
return super().save(commit)

Expand Down Expand Up @@ -463,8 +461,7 @@ class ExternalSiteForm(VisualForm):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

data = json.loads(self.instance.settings)
data = self.instance.settings
if "external_url" in data:
self.fields["external_url"].initial = data["external_url"]
if "filters" in data:
Expand All @@ -473,14 +470,12 @@ def __init__(self, *args, **kwargs):
self.helper = self.setHelper()

def save(self, commit=True):
self.instance.settings = json.dumps(
dict(
external_url=self.cleaned_data["external_url"],
external_url_hostname=self.cleaned_data["external_url_hostname"],
external_url_path=self.cleaned_data["external_url_path"],
external_url_query_args=self.cleaned_data["external_url_query_args"],
filters=json.loads(self.cleaned_data["filters"]),
)
self.instance.settings = dict(
external_url=self.cleaned_data["external_url"],
external_url_hostname=self.cleaned_data["external_url_hostname"],
external_url_path=self.cleaned_data["external_url_path"],
external_url_query_args=self.cleaned_data["external_url_query_args"],
filters=json.loads(self.cleaned_data["filters"]),
)
return super().save(commit)

Expand Down Expand Up @@ -566,7 +561,7 @@ def clean_settings(self):
# we remove <extra> tag; by default it's included in plotly visuals but it doesn't pass
# our html validation checks
settings: str = (
self.cleaned_data.get("settings", "")
json.dumps(self.cleaned_data.get("settings", ""))
.strip()
.replace("<extra>", "")
.replace("</extra>", "")
Expand All @@ -583,7 +578,7 @@ def clean_settings(self):
pio.from_json(settings)
except ValueError as err:
raise forms.ValidationError("Invalid Plotly figure") from err
return settings
return json.loads(settings)


def get_visual_form(visual_type):
Expand Down
43 changes: 43 additions & 0 deletions hawc/apps/summary/migrations/0049_alter_visual_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Generated by Django 4.2.3 on 2023-09-21 20:38
import json

from django.db import migrations, models


def settings_json(apps, schema_editor):
Visual = apps.get_model("summary", "Visual")
changed = []
for visual in Visual.objects.all():
try:
json.loads(visual.settings)
except (TypeError, ValueError):
visual.settings = {}
changed.append(visual)
if changed:
Visual.objects.bulk_update(changed, ["settings"])


def reverse_settings_json(apps, schema_editor):
Visual = apps.get_model("summary", "Visual")
changed = []
for visual in Visual.objects.all():
settings = json.loads(visual.settings)
if len(settings) == 0:
visual.settings = "undefined"
if changed:
Visual.objects.bulk_update(changed, ["settings"])


class Migration(migrations.Migration):
dependencies = [
("summary", "0048_visual_studies_to_prefilters"),
]

operations = [
migrations.RunPython(settings_json, reverse_code=reverse_settings_json),
migrations.AlterField(
model_name="visual",
name="settings",
field=models.JSONField(default=dict),
),
]
12 changes: 3 additions & 9 deletions hawc/apps/summary/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ class Visual(models.Model):
help_text="Endpoints to be included in visualization",
blank=True,
)
settings = models.TextField(default="{}")
settings = models.JSONField(default=dict)
caption = models.TextField(blank=True)
published = models.BooleanField(
default=False,
Expand Down Expand Up @@ -449,12 +449,6 @@ def get_heatmap_datasets(cls, assessment: Assessment) -> HeatmapDatasets:
def get_dose_units():
return DoseUnits.objects.json_all()

def get_settings(self) -> dict | None:
try:
return json.loads(self.settings)
except ValueError:
return None

def get_json(self, json_encode=True):
return SerializerHelper.get_serialized(self, json=json_encode)

Expand Down Expand Up @@ -603,13 +597,13 @@ def get_plotly_from_json(self) -> Figure:
if self.visual_type != constants.VisualType.PLOTLY:
raise ValueError("Incorrect visual type")
try:
return from_json(self.settings)
return from_json(json.dumps(self.settings))
except ValueError as err:
raise ValueError(err)

def _rob_data_qs(self, use_settings: bool = True) -> models.QuerySet:
study_ids = list(self.get_studies().values_list("id", flat=True))
settings = json.loads(self.settings)
settings = self.settings

qs = RiskOfBiasScore.objects.filter(
riskofbias__active=True,
Expand Down
Loading
Loading