Add canonical field and update synonym handling in Condition model

- Introduced a new `canonical` ForeignKey field in the Condition model to manage alias relationships.
- Enhanced synonym retrieval methods to prefer canonical names and handle aliases more effectively.
- Updated condition detail templates to reflect canonical status and improved synonym display.
- Implemented migration scripts to establish canonical relationships based on existing synonym links.
This commit is contained in:
Ross
2025-11-20 22:48:03 +00:00
parent 9d57d03cbb
commit 6eeb4cbb1d
8 changed files with 199 additions and 29 deletions
+74 -11
View File
@@ -116,30 +116,66 @@ class SynMixin(object):
return f"{self.name} [syn]"
def get_old_str(self) -> str:
# Prefer using a model-specific get_synonyms (e.g. Condition.get_synonyms)
try:
syns = self.get_synonyms()
except Exception:
syns = None
if self.primary:
if self.synonym.count() == 0:
return self.name
else:
if syns is None:
if getattr(self, "synonym", None) is None or self.synonym.count() == 0:
return self.name
synonyms = ",".join([i.name for i in self.synonym.all()])
return f"{self.name} ({synonyms})"
else:
if not syns:
return self.name
synonyms = ",".join([i.name for i in syns])
return f"{self.name} ({synonyms})"
else:
return f"{self.name} [syn]"
def get_synonym(self):
if self.primary:
# Return a readable synonym string. For models that implement
# get_synonyms (Condition with canonical/aliases), use that.
try:
syns = self.get_synonyms()
except Exception:
syns = None
if getattr(self, "primary", False):
return "[Primary]"
else:
s = self.synonym.filter(primary=True).values_list("name", flat=True)
return ", ".join(s)
if syns is None:
s = self.synonym.filter(primary=True).values_list("name", flat=True)
return ", ".join(s)
else:
# prefer primary names among synonyms if present
primary_names = [i.name for i in syns if getattr(i, "primary", False)]
if primary_names:
return ", ".join(primary_names)
return ", ".join([i.name for i in syns])
def get_synonym_link(self):
if self.primary:
try:
syns = self.get_synonyms()
except Exception:
syns = None
if getattr(self, "primary", False):
return "[Primary]"
else:
syns = self.synonym.filter(primary=True)
return ", ".join(
[f"<a href='{s.get_absolute_url()}'>{s.name}</a>" for s in syns]
)
if syns is None:
# fall back to all M2M synonyms (not just primary) for models
# that still use the old synonym field (e.g. Finding, Structure)
syns_qs = self.synonym.all()
items = [f"<a href='{s.get_absolute_url()}'>{s.name}</a>" for s in syns_qs]
return ", ".join(items)
else:
# render links for synonyms/canonical group
items = [f"<a href='{s.get_absolute_url()}'>{s.name}</a>" for s in syns]
return ", ".join(items)
def get_link(self):
return format_html("<a href='{}'>{}</a>", self.get_absolute_url(), self.name)
@@ -163,6 +199,16 @@ class Condition(SynMixin, models.Model):
blank=True,
help_text="Use if a direct synonym for the condition exists, e.g. 'Wegener granulomatosis' and 'Granulomatosis with Polyangitis'.",
)
# New canonical/alias field (Option B): if set, this Condition is an alias
# and points to the canonical/master Condition.
canonical = models.ForeignKey(
"self",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="aliases",
help_text="If set, this Condition is an alias and points to the canonical Condition.",
)
parent = models.ManyToManyField(
"self",
blank=True,
@@ -198,6 +244,23 @@ class Condition(SynMixin, models.Model):
def get_absolute_url(self):
return reverse("atlas:condition_detail", kwargs={"pk": self.pk})
@property
def canonical_condition(self):
"""Return the canonical/master condition for this Condition (self if none)."""
return self.canonical if self.canonical else self
def get_synonyms(self):
"""Return other Conditions that are synonyms/aliases for this concept.
Behaviour:
- If this Condition is an alias (canonical set), return the canonical and
any other aliases (excluding self).
- If this Condition is canonical, return all aliases (and exclude self).
"""
master = self.canonical_condition
qs = Condition.objects.filter(models.Q(canonical=master) | models.Q(pk=master.pk)).exclude(pk=self.pk)
return qs
def get_children(self):
return self.child.all()