Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enable italicisation of portion #2312

Merged
merged 30 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6288294
enable italicisation of portion
goose-life Dec 13, 2024
a2e64fc
include portionBody in TextPatternMarker'a ancestor elements
goose-life Jan 10, 2025
7e44d4c
new DocumentAPISerializer
goose-life Jan 10, 2025
4c702f7
validate XML, improve updated_xml()
goose-life Jan 10, 2025
f9345ad
update MarkUpItalicsTermsView to use new serializer
goose-life Jan 10, 2025
3a24517
add and use wrapInAkn, toSimplifiedJSON to DocumentContent
goose-life Jan 10, 2025
b3009f4
new ManipulateXmlView superclass for all four views
goose-life Jan 13, 2025
645c0ba
other JS views use the new pattern too
goose-life Jan 13, 2025
4dcc5d5
update existing, add new test_link_terms
goose-life Jan 13, 2025
24657e5
latest docpipe (portionBody included in ancestors)
goose-life Jan 13, 2025
a93766f
Merge branch 'live-editing' into portion
goose-life Jan 15, 2025
93071d0
Merge branch 'master' into portion
goose-life Jan 16, 2025
2c6d54d
set the content once when removing terms, italics, references
goose-life Jan 16, 2025
e0d1aed
add get_provision_xml to Document model
goose-life Jan 16, 2025
00934e9
use provision_eid, not is_portion
goose-life Jan 16, 2025
a0beafb
introduce use_full_xml for analyses that require the whole document
goose-life Jan 16, 2025
6bb4931
replace XML rather than de- and re-serializing
goose-life Jan 16, 2025
038a245
Update indigo_app/views/documents.py
goose-life Jan 16, 2025
6717916
Update indigo_api/models/documents.py
goose-life Jan 16, 2025
7b10446
update test, provision_eid must (and will always) match an existing eid
goose-life Jan 16, 2025
b705695
fix replacements
goose-life Jan 16, 2025
7b078a8
get_provision_xml --> get_provision_element
goose-life Jan 16, 2025
d0bb190
fix terms across the document before saving, update test
goose-life Jan 16, 2025
2ea0191
do link_terms_in_provision rather
goose-life Jan 16, 2025
b9e75a8
update template wording in provision mode
goose-life Jan 16, 2025
ec891e0
rename method
goose-life Jan 17, 2025
ec20924
Update indigo_app/templates/indigo_api/document/_defined_terms.html
goose-life Jan 17, 2025
5ed13de
Update indigo_app/templates/indigo_api/document/_defined_terms.html
goose-life Jan 17, 2025
5f8ba8d
fix test
goose-life Jan 17, 2025
a59dc52
deal with missing provision_eid differently
goose-life Jan 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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_xml(self, provision_eid):
longhotsummer marked this conversation as resolved.
Show resolved Hide resolved
provision_xml = self.doc.get_portion_element(provision_eid)
if not provision_xml:
goose-life marked this conversation as resolved.
Show resolved Hide resolved
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_xml(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
76 changes: 58 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 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,84 @@
</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):

Check failure on line 72 in indigo_api/tests/test_analysis_api.py

View workflow job for this annotation

GitHub Actions / Test Results

test_link_terms_portion (indigo_api.tests.test_analysis_api.AnalysisTestCase) with error

test-reports/TEST-indigo_api.tests.test_analysis_api.AnalysisTestCase-20250116094012.xml [took 0s]
Raw output
'NoneType' object has no attribute 'getparent'
Traceback (most recent call last):
  File "/home/runner/work/indigo/indigo/indigo_api/tests/test_analysis_api.py", line 73, in test_link_terms_portion
    response = self.client.post('/api/documents/1/analysis/link-terms', {
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/test.py", line 295, in post
    response = super().post(
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/test.py", line 209, in post
    return self.generic('POST', path, data, content_type, **extra)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/test.py", line 233, in generic
    return super().generic(
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/django/test/client.py", line 609, in generic
    return self.request(**r)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/test.py", line 285, in request
    return super().request(**kwargs)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/test.py", line 237, in request
    request = super().request(**kwargs)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/django/test/client.py", line 891, in request
    self.check_exception(response)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/django/test/client.py", line 738, in check_exception
    raise exc_value
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/contextlib.py", line 79, in inner
    return func(*args, **kwds)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/django/views/decorators/csrf.py", line 56, in wrapper_view
    return view_func(*args, **kwargs)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/django/views/generic/base.py", line 104, in view
    return self.dispatch(request, *args, **kwargs)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "/opt/hostedtoolcache/Python/3.10.16/x64/lib/python3.10/site-packages/rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "/home/runner/work/indigo/indigo/indigo_api/views/documents.py", line 374, in post
    serializer.update_document()
  File "/home/runner/work/indigo/indigo/indigo_api/serializers.py", line 464, in update_document
    self.set_content()
  File "/home/runner/work/indigo/indigo/indigo_api/serializers.py", line 477, in set_content
    document.update_provision_xml(provision_eid, xml)
  File "/home/runner/work/indigo/indigo/indigo_api/models/documents.py", line 565, in update_provision_xml
    old_provision.getparent().replace(old_provision, updated_provision)
AttributeError: 'NoneType' object has no attribute 'getparent'
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': "section-1"
})
assert_equal(response.status_code, 200)

content = response.data['xml']

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

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
Loading