Skip to content

Commit

Permalink
Merge pull request #2312 from laws-africa/portion
Browse files Browse the repository at this point in the history
enable italicisation of portion
  • Loading branch information
goose-life authored Jan 17, 2025
2 parents 4a59281 + a59dc52 commit 3647062
Show file tree
Hide file tree
Showing 17 changed files with 298 additions and 139 deletions.
2 changes: 1 addition & 1 deletion indigo/analysis/markup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class TextPatternMarker:
""" Xpath for candidate text nodes that should be tested for matches. Must be defined by subclasses.
"""

ancestors = ['coverpage', 'preface', 'preamble', 'body', 'mainBody', 'conclusions']
ancestors = ['coverpage', 'preface', 'preamble', 'body', 'mainBody', 'portionBody', 'conclusions']
""" Tags that the candidate_xpath should be run against.
"""

Expand Down
10 changes: 10 additions & 0 deletions indigo/analysis/terms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ def find_terms(self, doc):
self.find_term_references(doc, terms)
self.renumber_terms(doc)

def link_terms_in_document(self, document):
""" Passively link defined terms in a Document while editing in provision mode.
"""
root = etree.fromstring(document.doc.to_xml(encoding='unicode'))
self.setup(root)
terms = self.find_definitions(root)
self.find_term_references(root, terms)
self.renumber_terms(root)
document.content = etree.tostring(root, encoding='unicode')

def setup(self, doc):
self.ns = doc.nsmap[None]
self.nsmap = {'a': self.ns}
Expand Down
9 changes: 9 additions & 0 deletions indigo_api/models/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,15 @@ def change_date(self, new_date, user, comment=None):
self.expression_date = new_date
self.save_with_revision(user, comment=comment)

def get_provision_element(self, provision_eid):
provision_xml = self.doc.get_portion_element(provision_eid)
if provision_xml is None:
return None
portion = StructuredDocument.for_document_type('portion')()
portion.frbr_uri = self.frbr_uri
portion.main_content.append(provision_xml)
return portion.main

def update_provision_xml(self, provision_eid, provision_xml):
xml = etree.fromstring(provision_xml)
# portionBody will always have exactly one child
Expand Down
79 changes: 63 additions & 16 deletions indigo_api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import logging
import os.path
from itertools import groupby
from typing import List

from actstream.signals import action
from collections import OrderedDict

from lxml.etree import LxmlError

from django.contrib.auth.models import User
import logging
import reversion
from actstream.signals import action
from allauth.account.utils import user_display
from django.conf import settings
from django.contrib.auth.models import User
from lxml import etree
from lxml.etree import LxmlError
from rest_framework import serializers
from rest_framework.reverse import reverse
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse
from typing import List

from cobalt import StructuredDocument, FrbrUri
from cobalt.akn import AKN_NAMESPACES, DEFAULT_VERSION
import reversion

from indigo_api.models import Document, Attachment, Annotation, DocumentActivity, Work, Amendment, Language, \
PublicationDocument, Task, Commencement
from indigo_api.signals import document_published
from allauth.account.utils import user_display

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -438,11 +436,60 @@ class DocumentAPISerializer(serializers.Serializer):
"""
Helper to handle input documents for general document APIs
"""
document = DocumentSerializer(required=True)
xml = serializers.CharField()
language = serializers.CharField(min_length=3, max_length=3)
provision_eid = serializers.CharField(allow_blank=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['document'].instance = self.instance
def validate_xml(self, xml):
""" mostly copied from DocumentSerializer.validate()
"""
try:
doctype = self.instance.work_uri.doctype if not self.initial_data.get('provision_eid') else 'portion'
doc = StructuredDocument.for_document_type(doctype)(xml)
except (LxmlError, ValueError) as e:
raise ValidationError("Invalid XML: %s" % str(e))
# ensure the correct namespace
if doc.namespace != AKN_NAMESPACES[DEFAULT_VERSION]:
raise ValidationError(
f"Document must have namespace {AKN_NAMESPACES[DEFAULT_VERSION]}, but it has {doc.namespace} instead.")
return xml

def update_document(self):
document = self.instance
# the language could have been changed but not yet saved during editing, which might affect which plugin is used
language_code = self.validated_data.get('language')
if document.language.code != language_code:
document.language = Language.for_code(language_code)
# update the content with updated but unsaved changes too
self.set_content()

def set_content(self):
document = self.instance
xml = self.validated_data.get('xml')
provision_eid = self.validated_data.get('provision_eid')
if not provision_eid:
# update the document's full XML with what's in the editor
document.content = xml
else:
if self.use_full_xml:
# we'll need the document's full XML for the analysis,
# but update the relevant provision with what's in the editor first
document.update_provision_xml(provision_eid, xml)
else:
# update the document to be a 'portion' and use only what's in the editor as the content
document.work.work_uri.doctype = 'portion'
document.content = xml

def updated_xml(self):
document = self.instance
provision_eid = self.validated_data.get('provision_eid')
if not provision_eid:
# return the document's full updated XML
return document.document_xml
# otherwise, return only the provision being edited (NOT including the outer akn tag)
# if we used the full XML for the analysis, grab only the appropriate provision as a portion
xml = document.get_provision_element(provision_eid) if self.use_full_xml else document.doc.portion
return etree.tostring(xml, encoding='unicode')


class NoopSerializer(object):
Expand Down
42 changes: 42 additions & 0 deletions indigo_api/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,41 @@
</akomaNtoso>
"""

PORTION_FIXTURE = """<akomaNtoso xmlns="http://docs.oasis-open.org/legaldocml/ns/akn/3.0">
<portion name="portion">
<meta>
<identification source="">
<FRBRWork>
<FRBRthis value="/akn/za/act/1900/1/!main"/>
<FRBRuri value="/akn/za/act/1900/1"/>
<FRBRalias value="Untitled"/>
<FRBRdate date="1900-01-01" name="Generation"/>
<FRBRauthor href="#council" as="#author"/>
<FRBRcountry value="za"/>
</FRBRWork>
<FRBRExpression>
<FRBRthis value="/akn/za/act/1900/1/eng@/!main"/>
<FRBRuri value="/akn/za/act/1900/1/eng@"/>
<FRBRdate date="1900-01-01" name="Generation"/>
<FRBRauthor href="#council" as="#author"/>
<FRBRlanguage language="eng"/>
</FRBRExpression>
<FRBRManifestation>
<FRBRthis value="/akn/za/act/1900/1/eng@/!main"/>
<FRBRuri value="/akn/za/act/1900/1/eng@"/>
<FRBRdate date="1900-01-01" name="Generation"/>
<FRBRauthor href="#council" as="#author"/>
</FRBRManifestation>
</identification>
<publication date="2005-07-24" name="Province of Western Cape: σπαθιοῦ Gazette" number="6277" showAs="Province of Western Cape: 😀 Provincial Gazette"/>
</meta>
<portionBody>
%s
</portionBody>
</portion>
</akomaNtoso>
"""

BODY_FIXTURE = """
<body>
<section eId="sec_1">
Expand Down Expand Up @@ -130,6 +165,13 @@ def document_fixture(text=None, xml=None):
return DOCUMENT_FIXTURE % xml


def portion_fixture(text=None, xml=None):
if text:
xml = """<section eId="sec_1"><content><p>%s</p></content></section>""" % text

return PORTION_FIXTURE % xml


def component_fixture(text=None, xml=None):
if text:
xml = """<section eId="sec_1"><content><p>%s</p></content></section>""" % text
Expand Down
75 changes: 57 additions & 18 deletions indigo_api/tests/test_analysis_api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-

from nose.tools import * # noqa
from rest_framework.test import APITestCase

Expand All @@ -19,17 +17,12 @@ def setUp(self):

def test_link_terms_no_input(self):
response = self.client.post('/api/documents/1/analysis/link-terms', {
'document': {},
})
assert_equal(response.status_code, 400)

def test_link_terms(self):
response = self.client.post('/api/documents/1/analysis/link-terms', {
'document': {
'frbr_uri': '/za/act/1992/1',
'expression_date': '2001-01-01',
'language': 'eng',
'content': document_fixture(xml="""
'xml': document_fixture(xml="""
<section id="section-1">
<num>1.</num>
<heading>Definitions and interpretation</heading>
Expand Down Expand Up @@ -64,37 +57,83 @@ def test_link_terms(self):
</content>
</subsection>
</section>
""")
}
"""),
'language': 'eng',
'provision_eid': ""
})
assert_equal(response.status_code, 200)

content = response.data['document']['content']
content = response.data['xml']

assert_true(content.startswith('<akomaNtoso'))
assert_in('<def ', content)
assert_in('<TLCTerm ', content)

def test_link_terms_portion(self):
response = self.client.post('/api/documents/1/analysis/link-terms', {
'xml': portion_fixture(xml="""
<section id="section-1">
<num>1.</num>
<heading>Definitions and interpretation</heading>
<subsection id="section-1.subsection-0">
<content>
<p>In these By-laws, any word or expression that has been defined in the National Road Traffic Act, 1996 (Act No. 93 of 1996) has that meaning and, unless the context otherwise indicates –</p>
</content>
</subsection>
<subsection id="section-1.subsection-1">
<content>
<blockList id="section-1.subsection-1.list1">
<listIntroduction>"authorised official" means –</listIntroduction>
<item id="section-1.subsection-1.list1.a">
<num>(a)</num>
<p>a member of the Johannesburg Metropolitan Police established in terms of section 64A of the South African Police Service Act, 1995 (Act No. 68 of 1995); or</p>
</item>
<item id="section-1.subsection-1.list1.b">
<num>(b)</num>
<p>any person or official authorised as such, in writing, by the Council;</p>
</item>
</blockList>
</content>
</subsection>
<subsection id="section-1.subsection-2">
<content>
<p>"backfill" means to replace the structural layers, including the base, sub-base, sudgrade and subgrade but excluding the surfacing, in a trench dug in, or other excavation of, a road reserve, and “backfilling” is construed accordingly;</p>
</content>
</subsection>
<subsection id="section-1.subsection-3">
<content>
<p>"these By-Laws" includes the schedules;</p>
</content>
</subsection>
</section>
"""),
'language': 'eng',
'provision_eid': "sec_1"
})
assert_equal(response.status_code, 200)

content = response.data['xml']

assert_true(content.startswith('<portion '))
# definitions aren't found, only linked, in provision mode

def test_link_terms_no_perms(self):
self.client.logout()
self.assertTrue(self.client.login(username='no-perms@example.com', password='password'))
user = User.objects.get(username='no-perms@example.com')

data = {
'document': {
'frbr_uri': '/za/act/1992/1',
'expression_date': '2001-01-01',
'language': 'eng',
'content': document_fixture(xml="""
'xml': document_fixture(xml="""
<section id="section-1">
<num>1.</num>
<heading>Definitions and interpretation</heading>
<content>
<p>test</p>
</content>
</section>
""")
}
"""),
'language': 'eng',
'provision_eid': ""
}

# user doesn't have perms
Expand Down
Loading

0 comments on commit 3647062

Please sign in to comment.