Skip to content

Commit

Permalink
CPHEAPM-27 Add ToxRefDB to Update Vocab Endpoint (#1075)
Browse files Browse the repository at this point in the history
* feat: add ToxRef vocab fixture and migration, create a new vocabulary page

* test: create toxref vocab tests

* fix: refactor vocab additions

* update vocab files for testing and run lint

* fix: account for undefined vocab

* update: implement toxrefdb in 'update modules'

* update the endpoint model to account for ToxRefDB

* bump django version minimum

* minor edits from code review

* fix api endpoint

* add staticmethod props

* text updates

* remove debugging

* revise lookups

* use a lookup

* format

---------

Co-authored-by: Zindah, Farha <63080@icf.com>
Co-authored-by: Andy Shapiro <shapiromatron@gmail.com>
  • Loading branch information
3 people authored Sep 10, 2024
1 parent 87f9d7f commit 3d9a079
Show file tree
Hide file tree
Showing 14 changed files with 105 additions and 93 deletions.
13 changes: 2 additions & 11 deletions frontend/animal/EndpointForm/TermSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class TermSelector extends Component {
store,
parentRequired,
} = this.props,
{object, debug, vocabulary_display} = store.config,
{object, vocabulary_display} = store.config,
useControlledVocabulary = store.useControlledVocabulary[termTextField],
currentId = object[termIdField],
currentText = object[termTextField],
Expand All @@ -50,9 +50,7 @@ class TermSelector extends Component {
style={{maxWidth: 130}}
value={this.state.idLookupValue}
onChange={event =>
this.setState({
idLookupValue: parseInt(event.target.value),
})
this.setState({idLookupValue: parseInt(event.target.value)})
}
/>
<div className="input-group-append">
Expand Down Expand Up @@ -136,13 +134,6 @@ class TermSelector extends Component {
className="form-text text-muted"
dangerouslySetInnerHTML={{__html: helpText}}></small>
) : null}
{debug ? (
<ul>
<li>termId: {currentId}</li>
<li>text: {currentText}</li>
<li>parent: {object[parentIdField]}</li>
</ul>
) : null}
</div>
);
}
Expand Down
24 changes: 11 additions & 13 deletions frontend/animal/EndpointForm/components/VocabTermFields.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,17 @@ class VocabTermFields extends Component {
rowClass = label.organ ? "col-md-3" : "col-md-4";
return (
<>
{label.endpoint_name ? (
<TermSelector
name={"name"}
label={label.endpoint_name}
helpText={helpTextName}
popupHelpText={helpText.endpoint_name_popup}
termIdField={"name_term_id"}
termTextField={"name"}
parentIdField={termParent.endpoint_name_parent}
parentRequired={true}
idLookupAction={store.endpointNameLookup}
/>
) : null}
<TermSelector
name={"name"}
label={label.endpoint_name}
helpText={helpTextName}
popupHelpText={helpText.endpoint_name_popup}
termIdField={"name_term_id"}
termTextField={"name"}
parentIdField={termParent.endpoint_name_parent}
parentRequired={true}
idLookupAction={store.endpointNameLookup}
/>

<div className="row">
<div className={rowClass}>
Expand Down
2 changes: 2 additions & 0 deletions frontend/animal/EndpointForm/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const termUrlLookupMap = {
"effect_term_id-2": "/vocab/api/toxrefdb/effect/?format=json",
"effect_subtype_term_id-2": "/vocab/api/toxrefdb/effect_subtype/?format=json",
"name_term_id-2": "/vocab/api/toxrefdb/endpoint_name/?format=json",
"id-lookup-1": "/vocab/api/ehv/:id/endpoint-name-lookup/",
"id-lookup-2": "/vocab/api/toxrefdb/:id/endpoint-name-lookup/",
},
termUrlLookup = function(term, vocab) {
return termUrlLookupMap[`${term}-${vocab}`];
Expand Down
4 changes: 3 additions & 1 deletion frontend/animal/EndpointForm/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import _ from "lodash";
import {action, computed, observable} from "mobx";
import h from "shared/utils/helpers";

import {termUrlLookup} from "./constants";

const fields = ["system", "organ", "effect", "effect_subtype", "name"];

class EndpointFormStore {
Expand Down Expand Up @@ -34,7 +36,7 @@ class EndpointFormStore {

@action.bound
endpointNameLookup(val) {
const url = `/vocab/api/ehv/${val}/endpoint-name-lookup/`;
const url = termUrlLookup("id-lookup", this.config.vocabulary).replace(":id", val);

if (!val) {
return;
Expand Down
4 changes: 0 additions & 4 deletions frontend/shared/components/AutocompleteTerm.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ import {
} from "./Autocomplete/constants";

class AutocompleteTerm extends Component {
/*
Autocomplete widget; works with `hawc.apps.vocab.api.EhvTermViewSet`
*/

constructor(props) {
super(props);
this.state = {
Expand Down
13 changes: 3 additions & 10 deletions hawc/apps/animal/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,14 +453,9 @@ def helper(self):
if vocab_enabled:
vocab_id = self.instance.assessment.vocabulary
vocab_url = VocabularyNamespace(vocab_id).display_url
vocab = f"""&nbsp;The <a href="{vocab_url}">
{VocabularyNamespace(vocab_id).display_name}</a> is enabled for this assessment. Browse to view
controlled terms, and whenever possible please use these terms."""
vocab = f"""The <a href="{vocab_url}">{VocabularyNamespace(vocab_id).display_name}</a> is enabled for this assessment. Browse to view controlled terms, and whenever possible please use these terms."""
else:
vocab = f"""&nbsp;A controlled vocabulary is not enabled for this assessment.
However, you can still browse the <a href="{reverse('vocab:ehv-browse')}"></a>
to see if this vocabulary would be a good fit for your
assessment. If it is, consider updating the assessment to use this vocabulary."""
vocab = f"""A controlled vocabulary is not enabled for this assessment. However, you can still browse the <a href="{reverse('vocab:ehv-browse')}">EHV</a> to see if this vocabulary would be a good fit for your assessment."""

if self.instance.id:
inputs = {
Expand All @@ -471,9 +466,7 @@ def helper(self):
else:
inputs = {
"legend_text": "Create new endpoint",
"help_text": f"""Create a new endpoint. An endpoint may should describe one
measure-of-effect which was measured in the study. It may
or may not contain quantitative data.{vocab}""",
"help_text": f"""Create a new endpoint. An endpoint should describe one measure-of-effect in the study. It may or may not contain quantitative data. {vocab}""",
"cancel_url": self.instance.animal_group.get_absolute_url(),
}

Expand Down
10 changes: 4 additions & 6 deletions hawc/apps/animal/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,13 +481,11 @@ def _validate_update_terms(self, objs: list[dict], assessment: Assessment):
@transaction.atomic
def update_terms(self, objs: list[dict], assessment: Assessment) -> list:
"""
Updates all of the terms and respective text fields
for a list of endpoints based on the given name term.
All endpoints must be from the same assessment.
Updates all of the terms and respective text fields for a list of endpoints based on the
given name term. All endpoints must be from the same assessment.
Args:
objs (list[dict]): List of endpoint dicts, where each dict has
for keys 'id' and 'name_term_id'
objs (list[dict]): Endpoint dicts, where each dict has keys "id" and "name_term_id"
assessment (Assessment): Assessment for endpoints
Returns:
Expand All @@ -500,7 +498,7 @@ def update_terms(self, objs: list[dict], assessment: Assessment) -> list:
endpoint_id_to_term_id = {obj["id"]: obj["name_term_id"] for obj in objs}

# set endpoint terms
terms_df = Term.ehv_dataframe()
terms_df = Term.vocab_dataframe(assessment.vocabulary)
endpoint_ids = [obj["id"] for obj in objs]
endpoints = self.get_queryset().filter(pk__in=endpoint_ids)
type_to_text_field = VocabularyTermType.value_to_text_field()
Expand Down
8 changes: 4 additions & 4 deletions hawc/apps/animal/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
tryParseInt,
)
from ..study.models import Study
from ..vocab.constants import VocabularyNamespace
from ..vocab.models import Term
from . import constants, managers

Expand Down Expand Up @@ -806,12 +807,11 @@ def unique_items(els):

@classmethod
def get_vocabulary_settings(cls, assessment: Assessment, form: ModelForm) -> str:
vocab = VocabularyNamespace(assessment.vocabulary) if assessment.vocabulary else None
return json.dumps(
{
"debug": False,
"vocabulary": assessment.vocabulary,
"vocabulary_url": assessment.get_vocabulary_url(),
"vocabulary_display": assessment.get_vocabulary_display(),
"vocabulary": vocab.value if vocab else None,
"vocabulary_display": vocab.display_name if vocab else None,
"object": {
"system": form["system"].value() or "",
"organ": form["organ"].value() or "",
Expand Down
11 changes: 2 additions & 9 deletions hawc/apps/assessment/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,15 +366,8 @@ def user_is_project_manager_or_higher(self, user) -> bool:
perms = self.get_permissions()
return perms.project_manager_or_higher(user)

def get_vocabulary_display(self) -> str:
# override default method
if self.vocabulary:
return VocabularyNamespace(self.vocabulary).display_name
else:
return ""

def get_vocabulary_url(self) -> str | None:
return VocabularyNamespace(self.vocabulary).display_url if self.vocabulary else None
def get_vocabulary_display(self) -> str | None:
return VocabularyNamespace(self.vocabulary).display_name if self.vocabulary else None

def get_noel_names(self) -> NoelNames:
if self.noel_name == constants.NoelName.NEL:
Expand Down
13 changes: 8 additions & 5 deletions hawc/apps/vocab/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,19 @@ def endpoint_name(self, request: Request) -> Response:

@action(detail=True, url_path="endpoint-name-lookup")
def endpoint_name_lookup(self, request: Request, pk: int) -> Response:
try:
term = models.Term.objects.get(
term = (
models.Term.objects.filter(
id=pk,
type=constants.VocabularyTermType.endpoint_name,
namespace=self.namespace,
deprecated_on__isnull=True,
)
except models.Term.DoesNotExist as err:
raise exceptions.NotFound() from err
return Response(term.vocab_endpoint_name())
.select_related("parent__parent__parent__parent")
.first()
)
if term is None:
raise exceptions.NotFound()
return Response(term.inheritance())

@action(detail=True, methods=("post",), url_path="related-entity")
def related_entity(self, request: Request, pk: int | None = None) -> Response:
Expand Down
25 changes: 12 additions & 13 deletions hawc/apps/vocab/constants.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from django.db.models import IntegerChoices
from django.urls import reverse_lazy
from django.utils.functional import classproperty
from django.urls import reverse


class VocabularyNamespace(IntegerChoices):
Expand All @@ -12,25 +11,25 @@ class VocabularyNamespace(IntegerChoices):
EHV = 1, "EHV"
ToxRefDB = 2, "ToxRefDB"

@classproperty
def display_dict(cls) -> dict:
return {1: "EPA Environmental health vocabulary", 2: "EPA ToxRefDB vocabulary"}

@classproperty
def display_urls(cls) -> dict:
return {1: reverse_lazy("vocab:ehv-browse"), 2: reverse_lazy("vocab:toxrefdb-browse")}

@classmethod
def display_choices(cls) -> list:
return [(key, value) for key, value in cls.display_dict.items()]
return [(item, item.display_name) for item in cls]

@property
def display_url(self) -> str:
return self.display_urls[self.value].lower()
match self:
case self.EHV:
return reverse("vocab:ehv-browse")
case self.ToxRefDB:
return reverse("vocab:toxrefdb-browse")

@property
def display_name(self) -> str:
return self.display_dict[self.value]
match self:
case self.EHV:
return "EPA Environmental health vocabulary"
case self.ToxRefDB:
return "EPA ToxRefDB vocabulary"


class VocabularyTermType(IntegerChoices):
Expand Down
54 changes: 38 additions & 16 deletions hawc/apps/vocab/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ def deprecated(self) -> bool:
def get_admin_edit_url(self) -> str:
return reverse("admin:vocab_term_change", args=(self.id,))

@classmethod
def vocab_dataframe(cls, namespace: constants.VocabularyNamespace) -> pd.DataFrame:
dataframes = {
constants.VocabularyNamespace.EHV: cls.ehv_dataframe,
constants.VocabularyNamespace.ToxRefDB: cls.toxrefdb_dataframe,
}
return dataframes[namespace]()

@classmethod
def ehv_dataframe(cls) -> pd.DataFrame:
vocab_data = cls.vocab_data(constants.VocabularyNamespace.EHV)
Expand All @@ -44,23 +52,21 @@ def ehv_dataframe(cls) -> pd.DataFrame:
{"df": vocab_data["effect_subtype"], "left_on": "effect_term_id"},
{"df": vocab_data["endpoint_name"], "left_on": "effect_subtype_term_id"},
]

df = cls.merge_data(vocab_data["system"], term_data)
return df

@classmethod
def toxrefdb_dataframe(cls) -> pd.DataFrame:
vocab_data = cls.vocab_data(constants.VocabularyNamespace.ToxRefDB)

term_data = [
{"df": vocab_data["effect"], "left_on": "system_term_id"},
{"df": vocab_data["effect_subtype"], "left_on": "effect_term_id"},
{"df": vocab_data["endpoint_name"], "left_on": "effect_subtype_term_id"},
]

df = cls.merge_data(vocab_data["system"], term_data)
return df

@staticmethod
def vocab_data(namespace) -> dict:
cols = ("id", "type", "parent_id", "name")
all_df = pd.DataFrame(
Expand Down Expand Up @@ -109,6 +115,7 @@ def vocab_data(namespace) -> dict:

return data

@staticmethod
def merge_data(df, data):
# merge df queries based on relevant hierarchy
for term in data:
Expand All @@ -119,19 +126,34 @@ def merge_data(df, data):
df = df.sort_values(by=[term["left_on"] for term in data]).reset_index(drop=True)
return df

def vocab_endpoint_name(self) -> dict:
return {
"system": self.parent.parent.parent.parent.name,
"organ": self.parent.parent.parent.name,
"effect": self.parent.parent.name,
"effect_subtype": self.parent.name,
"name": self.name,
"system_term_id": self.parent.parent.parent.parent.id,
"organ_term_id": self.parent.parent.parent.id,
"effect_term_id": self.parent.parent.id,
"effect_subtype_term_id": self.parent.id,
"name_term_id": self.id,
}
def inheritance(self) -> dict:
if self.namespace == constants.VocabularyNamespace.EHV:
return {
"system": self.parent.parent.parent.parent.name,
"organ": self.parent.parent.parent.name,
"effect": self.parent.parent.name,
"effect_subtype": self.parent.name,
"name": self.name,
"system_term_id": self.parent.parent.parent.parent.id,
"organ_term_id": self.parent.parent.parent.id,
"effect_term_id": self.parent.parent.id,
"effect_subtype_term_id": self.parent.id,
"name_term_id": self.id,
}
elif self.namespace == constants.VocabularyNamespace.ToxRefDB:
return {
"system": self.parent.parent.parent.name,
"organ": None,
"effect": self.parent.parent.name,
"effect_subtype": self.parent.name,
"name": self.name,
"system_term_id": self.parent.parent.parent.id,
"organ_term_id": None,
"effect_term_id": self.parent.parent.id,
"effect_subtype_term_id": self.parent.id,
"name_term_id": self.id,
}
raise ValueError(f"Invalid namespace: {self.namespace}")


class Entity(models.Model):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ classifiers = [
requires-python = ">=3.12"
dependencies = [
# web application
"Django~=5.1",
"Django~=5.1.1",
"django-crispy-forms==2.2",
"crispy-bootstrap4==2024.1",
"django-filter==24.2",
Expand Down
Loading

0 comments on commit 3d9a079

Please sign in to comment.