From aec74043b7a1e8ff1342660930311862313f28b4 Mon Sep 17 00:00:00 2001 From: Robbie1977 Date: Fri, 7 Aug 2026 10:01:04 +0000 Subject: [PATCH 1/2] Fall back to a term's own licence when building Licenses DataSet term info returns has_license on the term itself as `license` (QueryLibrary.dataset_term_info), not as `dataset_license`, but term_info_parse_object only read `dataset_license`. Every DataSet page therefore came back with "Licenses": {} and the client rendered no License row, even though the has_license edge was in the KB. Add the same fallback the dataclass serialiser (term_info_queries.VFBTerm.get_license) and the legacy Java serialiser (VFBProcessTermInfoJson.getLicense) already have. Source is left empty so the panel does not render a Source row linking back to the page you are already on. Add parity tests covering both the fallback and the unchanged dataset_license path. --- src/test/test_term_info_parity.py | 29 +++++++++++++++++++++++++++++ src/vfbquery/vfb_queries.py | 26 ++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/test/test_term_info_parity.py b/src/test/test_term_info_parity.py index fca8135e..fe56c83b 100644 --- a/src/test/test_term_info_parity.py +++ b/src/test/test_term_info_parity.py @@ -151,6 +151,35 @@ def test_license_term_info_does_not_5xx(self): "License term_info failed validity check") self.assertIn("License", result.get("SuperTypes", [])) + # --- Gap E: a term's own licence (`license`) must reach Licenses{} ----- + # DataSet term info returns has_license on the term itself as `license` + # (QueryLibrary.dataset_term_info), not as `dataset_license`. The + # serialiser only read `dataset_license`, so every dataset page rendered + # without a License row even though the edge was in the KB. + def test_dataset_own_license_reaches_licenses(self): + ti = self._parse("Cachero2010") + licenses = ti.get("Licenses", {}) + self.assertTrue(licenses, "DataSet own licence dropped from Licenses{}") + lic = licenses[0] + self.assertTrue(lic.get("short_form", "").startswith("VFBlicense"), + f"unexpected licence short_form: {lic.get('short_form')}") + self.assertTrue(lic.get("label"), "licence label missing") + self.assertTrue(lic.get("iri"), "licence iri missing") + + def test_dataset_own_license_has_no_self_source(self): + # The dataset is its own source, so leave source empty rather than + # rendering a Source row that links back to the same page. + lic = self._parse("Cachero2010").get("Licenses", {})[0] + self.assertEqual("", lic.get("source", "")) + self.assertEqual("", lic.get("source_iri", "")) + + def test_dataset_license_still_attributes_source_on_images(self): + # The dataset_license path is unchanged: an image/template still gets + # its licence via the dataset it came from, with that dataset as source. + licenses = self._parse("VFB_00101567").get("Licenses", {}) + self.assertTrue(licenses, "template lost its inherited licence") + self.assertTrue(licenses[0].get("source"), "inherited licence lost its source") + if __name__ == "__main__": unittest.main() diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index 9b7a166a..5366e42b 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -963,9 +963,31 @@ def term_info_parse_object(results, short_form): record['icon'] = dataset_license.license.icon record['source_iri'] = dataset_license.dataset.core.iri record['source'] = dataset_license.dataset.core.label - licenses[idx] = record + licenses[idx] = record termInfo["Licenses"] = licenses - + elif vfbTerm.license and len(vfbTerm.license) > 0: + # Terms that carry a licence directly rather than inheriting one + # from the dataset they came from. DataSet term info (see + # QueryLibrary.dataset_term_info) returns has_license on the term + # itself as `license`, with no `dataset_license`, so without this + # branch every dataset page loses its License row. + # Mirrors the fallback in term_info_queries.VFBTerm.get_license() + # and the legacy Java serialiser (VFBProcessTermInfoJson.getLicense). + licenses = {} + for idx, lcns in enumerate(vfbTerm.license): + record = {} + record['iri'] = lcns.core.iri + record['short_form'] = lcns.core.short_form + record['label'] = lcns.core.label + record['icon'] = lcns.icon + # No source dataset to attribute: the term is its own source, + # and an empty source keeps the panel from rendering a Source + # row that links back to the page you are already on. + record['source_iri'] = '' + record['source'] = '' + licenses[idx] = record + termInfo["Licenses"] = licenses + if vfbTerm.template_channel and vfbTerm.template_channel.channel.short_form: termInfo["IsTemplate"] = True images = {} From 541cc120d0f55f037035e40298a3f9a85c5a10bb Mon Sep 17 00:00:00 2001 From: Robbie1977 Date: Fri, 7 Aug 2026 10:13:49 +0000 Subject: [PATCH 2/2] Note why the licence fallback is elif rather than a merge --- src/vfbquery/vfb_queries.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index 5366e42b..db00572b 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -973,6 +973,9 @@ def term_info_parse_object(results, short_form): # branch every dataset page loses its License row. # Mirrors the fallback in term_info_queries.VFBTerm.get_license() # and the legacy Java serialiser (VFBProcessTermInfoJson.getLicense). + # Deliberately elif, not a merge: the query library populates one or + # the other per term type, and if a term ever carried both, the + # inherited dataset licence wins -- same precedence as get_license(). licenses = {} for idx, lcns in enumerate(vfbTerm.license): record = {}