From 491958dcacbefc47a32096ccbd4bc068207956e9 Mon Sep 17 00:00:00 2001 From: Tim Cowlishaw Date: Mon, 29 Jan 2024 20:10:54 +0100 Subject: [PATCH] Add methods to get and set judgment jurisdiction The General Regulatory Chamber has a number of Jurisdictions (or 'sub-tribunals' as we sometimes call them). These are different to the separate Lists we see within eg the Chancery Division - Lists are an administrative distinction, decided on by the court themselves, whereas Jurisdictions are a statutory one, decided in law. Neither jurisdiction nor list affects the NCN of the judgment, but they're stored differently in the XML - Lists are represented in the court code in the uk:court element, whereas judgments are represented with the uk:jurisdiction element in addition to the uk:court. This PR provides accessors for the jurisdiction data, both in isolation, and combined with the court as a slash-separated string, following the convention established in the courts metedata in ds-caselaw-utils. This will allow us to display the jurisdiction in the PUI (and allow editors to correct it in EUI) with minimal changes --- CHANGELOG.md | 1 + src/caselawclient/Client.py | 18 ++++++ src/caselawclient/models/documents.py | 14 ++++ .../xquery/set_metadata_jurisdiction.xqy | 37 +++++++++++ src/caselawclient/xquery_type_dicts.py | 6 ++ tests/client/test_get_set_metadata.py | 64 +++++++++++++++++++ tests/models/test_documents.py | 61 ++++++++++++++++++ 7 files changed, 201 insertions(+) create mode 100644 src/caselawclient/xquery/set_metadata_jurisdiction.xqy diff --git a/CHANGELOG.md b/CHANGELOG.md index 20cd3e4d..f1a40094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog 1.0.0]. ## Unreleased - **Breaking:** `Client.get_pending_enrichment_for_version` now requires both a target enrichment version and a target parser version, and will not include documents which have not been parsed with the target version. +- **Feature:** Add accessors for judgment jurisdiction ## [Release 20.0.0] diff --git a/src/caselawclient/Client.py b/src/caselawclient/Client.py index 007ac6ac..f77d206b 100644 --- a/src/caselawclient/Client.py +++ b/src/caselawclient/Client.py @@ -446,6 +446,24 @@ def set_document_court( return self._send_to_eval(vars, "set_metadata_court.xqy") + def set_document_jurisdiction( + self, document_uri: DocumentURIString, content: str + ) -> requests.Response: + uri = self._format_uri_for_marklogic(document_uri) + vars: query_dicts.SetMetadataJurisdictionDict = {"uri": uri, "content": content} + return self._send_to_eval(vars, "set_metadata_jurisdiction.xqy") + + def set_document_court_and_jurisdiction( + self, document_uri: DocumentURIString, content: str + ) -> requests.Response: + if "/" in content: + court, jurisdiction = re.split("\\s*/\\s*", content) + self.set_document_court(document_uri, court) + return self.set_document_jurisdiction(document_uri, jurisdiction) + else: + self.set_document_court(document_uri, content) + return self.set_document_jurisdiction(document_uri, "") + def set_judgment_this_uri( self, judgment_uri: DocumentURIString ) -> requests.Response: diff --git a/src/caselawclient/models/documents.py b/src/caselawclient/models/documents.py index e88b0a85..4674aca1 100644 --- a/src/caselawclient/models/documents.py +++ b/src/caselawclient/models/documents.py @@ -198,6 +198,20 @@ def court(self) -> str: }, ) + @cached_property + def jurisdiction(self) -> str: + return self.xml.get_xpath_match_string( + "/akn:akomaNtoso/akn:*/akn:meta/akn:proprietary/uk:jurisdiction/text()", + { + "uk": "https://caselaw.nationalarchives.gov.uk/akn", + "akn": "http://docs.oasis-open.org/legaldocml/ns/akn/3.0", + }, + ) + + @property + def court_and_jurisdiction(self) -> str: + return "/".join((self.court, self.jurisdiction)) + @cached_property def document_date_as_string(self) -> str: return self.xml.get_xpath_match_string( diff --git a/src/caselawclient/xquery/set_metadata_jurisdiction.xqy b/src/caselawclient/xquery/set_metadata_jurisdiction.xqy new file mode 100644 index 00000000..3c9f8d21 --- /dev/null +++ b/src/caselawclient/xquery/set_metadata_jurisdiction.xqy @@ -0,0 +1,37 @@ +xquery version "1.0-ml"; + +declare namespace akn = "http://docs.oasis-open.org/legaldocml/ns/akn/3.0"; +declare namespace uk = "https://caselaw.nationalarchives.gov.uk/akn"; + +declare variable $uri as xs:string external; +declare variable $content as xs:string external; +declare variable $proprietary-node := document($uri)/akn:akomaNtoso/akn:*/akn:meta/akn:proprietary; +declare variable $jurisdiction-node := $proprietary-node/uk:jurisdiction; + +declare function local:delete($uri) +{ + xdmp:node-delete($jurisdiction-node) +}; + +declare function local:edit($uri, $content) +{ + xdmp:node-replace( + $jurisdiction-node, + {$content} + ) +}; + +declare function local:add($uri, $content) +{ + xdmp:node-insert-child( + $proprietary-node, + {$content} + ) +}; + +if (fn:boolean( +cts:search(doc($uri), +cts:element-query(xs:QName('uk:jurisdiction'),cts:and-query(()))))) then + if ($content = "") then local:delete($uri) else local:edit($uri, $content) +else + local:add($uri, $content) diff --git a/src/caselawclient/xquery_type_dicts.py b/src/caselawclient/xquery_type_dicts.py index 543347c2..13a7c6d8 100644 --- a/src/caselawclient/xquery_type_dicts.py +++ b/src/caselawclient/xquery_type_dicts.py @@ -144,6 +144,12 @@ class SetMetadataCourtDict(MarkLogicAPIDict): uri: MarkLogicDocumentURIString +# set_metadata_jurisdiction.xqy +class SetMetadataJurisdictionDict(MarkLogicAPIDict): + content: str + uri: MarkLogicDocumentURIString + + # set_metadata_name.xqy class SetMetadataNameDict(MarkLogicAPIDict): content: str diff --git a/tests/client/test_get_set_metadata.py b/tests/client/test_get_set_metadata.py index e421ae60..b78ddaf2 100644 --- a/tests/client/test_get_set_metadata.py +++ b/tests/client/test_get_set_metadata.py @@ -64,6 +64,70 @@ def test_set_document_court(self): ) assert mock_eval.call_args.kwargs["vars"] == json.dumps(expected_vars) + def test_set_document_jurisdiction(self): + with patch.object(self.client, "eval") as mock_eval: + uri = "judgment/uri" + content = "new jurisdiction" + expected_vars = {"uri": "/judgment/uri.xml", "content": content} + self.client.set_document_jurisdiction(uri, content) + + assert mock_eval.call_args.args[0] == ( + os.path.join(ROOT_DIR, "xquery", "set_metadata_jurisdiction.xqy") + ) + assert mock_eval.call_args.kwargs["vars"] == json.dumps(expected_vars) + + def test_set_document_court_and_jurisdiction_when_both_passed(self): + # It splits the provided value on '/' + # and sets both court and jurisdiction + with patch.object(self.client, "eval") as mock_eval: + uri = "judgment/uri" + court_content = "court" + jurisdiction_content = "jurisdiction" + court_expected_vars = {"uri": "/judgment/uri.xml", "content": court_content} + jurisdiction_expected_vars = { + "uri": "/judgment/uri.xml", + "content": jurisdiction_content, + } + self.client.set_document_court_and_jurisdiction(uri, "court/jurisdiction") + + assert mock_eval.call_args_list[0].args[0] == ( + os.path.join(ROOT_DIR, "xquery", "set_metadata_court.xqy") + ) + assert mock_eval.call_args_list[0].kwargs["vars"] == json.dumps( + court_expected_vars + ) + + assert mock_eval.call_args_list[1].args[0] == ( + os.path.join(ROOT_DIR, "xquery", "set_metadata_jurisdiction.xqy") + ) + assert mock_eval.call_args_list[1].kwargs["vars"] == json.dumps( + jurisdiction_expected_vars + ) + + def test_set_document_court_and_jurisdiction_when_just_court_passed(self): + # When no jurisdiction is included + # It sets the court and deletes the jurisdiction. + with patch.object(self.client, "eval") as mock_eval: + uri = "judgment/uri" + content = "court" + court_expected_vars = {"uri": "/judgment/uri.xml", "content": content} + jurisdiction_expected_vars = {"uri": "/judgment/uri.xml", "content": ""} + self.client.set_document_court_and_jurisdiction(uri, content) + + assert mock_eval.call_args_list[0].args[0] == ( + os.path.join(ROOT_DIR, "xquery", "set_metadata_court.xqy") + ) + assert mock_eval.call_args_list[0].kwargs["vars"] == json.dumps( + court_expected_vars + ) + + assert mock_eval.call_args_list[1].args[0] == ( + os.path.join(ROOT_DIR, "xquery", "set_metadata_jurisdiction.xqy") + ) + assert mock_eval.call_args_list[1].kwargs["vars"] == json.dumps( + jurisdiction_expected_vars + ) + def test_set_document_date(self): with patch.object(self.client, "eval") as mock_eval: uri = "judgment/uri" diff --git a/tests/models/test_documents.py b/tests/models/test_documents.py index 75322eee..03f7a54a 100644 --- a/tests/models/test_documents.py +++ b/tests/models/test_documents.py @@ -543,6 +543,67 @@ def test_court(self, opening_tag, closing_tag, mock_api_client): "test/1234", show_unpublished=True ) + @pytest.mark.parametrize( + "opening_tag, closing_tag", + [ + ("judgment", "judgment"), + ('doc name="pressSummary"', "doc"), + ], + ) + def test_jurisdiction(self, opening_tag, closing_tag, mock_api_client): + mock_api_client.get_judgment_xml_bytestring.return_value = f""" + + <{opening_tag}> + + + SoftwareTesting + + + + + """.encode( + "utf-8" + ) + + document = Document("test/1234", mock_api_client) + + assert document.jurisdiction == "SoftwareTesting" + mock_api_client.get_judgment_xml_bytestring.assert_called_once_with( + "test/1234", show_unpublished=True + ) + + @pytest.mark.parametrize( + "opening_tag, closing_tag", + [ + ("judgment", "judgment"), + ('doc name="pressSummary"', "doc"), + ], + ) + def test_court_and_jurisdiction(self, opening_tag, closing_tag, mock_api_client): + mock_api_client.get_judgment_xml_bytestring.return_value = f""" + + <{opening_tag}> + + + UKFTT-CourtOfTesting + SoftwareTesting + + + + + """.encode( + "utf-8" + ) + + document = Document("test/1234", mock_api_client) + + assert document.court_and_jurisdiction == "UKFTT-CourtOfTesting/SoftwareTesting" + mock_api_client.get_judgment_xml_bytestring.assert_called_once_with( + "test/1234", show_unpublished=True + ) + @pytest.mark.parametrize( "opening_tag, closing_tag", [