diff --git a/README.rst b/README.rst index c4a581a..83e0449 100644 --- a/README.rst +++ b/README.rst @@ -92,6 +92,7 @@ Change Log - Cascade changes to FRBRlanguage into attachments - Don't hardcode source - Don't set ``contains="originalVersion"`` since it is the default value for that attribute. +- Use ``0001-01-01`` as a placeholder date for publication, amendment and repeal events with null dates 4.1.1 ..... diff --git a/cobalt/akn.py b/cobalt/akn.py index 401242b..67f9017 100644 --- a/cobalt/akn.py +++ b/cobalt/akn.py @@ -25,16 +25,29 @@ } DEFAULT_VERSION = '3.0' +# a placeholder date that indicates a null date, used in the XML where a date is required by may not be known +NULL_DATE = '0001-01-01' + def datestring(value): + """ Format a date as an XML-suitable string. If the date is None, uses NULL_DATE. + """ if value is None: - return "" + return NULL_DATE elif isinstance(value, str): return value else: return "%04d-%02d-%02d" % (value.year, value.month, value.day) +def parsedate(value): + """ Parse an XML date string into a real date. If the value is the NULL_DATE, returns None. + """ + if value == NULL_DATE: + return None + return parse_date(value).date() + + # Create a new objectify parser that doesn't remove blank text nodes objectify_parser = etree.XMLParser() objectify_parser.set_element_class_lookup(objectify.ObjectifyElementClassLookup()) @@ -91,7 +104,7 @@ def get_namespace(self): raise ValueError(f"Expected to find one of the following Akoma Ntoso XML namespaces: {', '.join(akn_namespaces)}. Only these namespaces were found: {', '.join(namespaces)}") - def ensure_element(self, name, after, at=None): + def ensure_element(self, name, after, at=None, attribs=None): """ Helper to get an element if it exists, or create it if it doesn't. :param name: dotted path from `self` or `at` @@ -101,7 +114,7 @@ def ensure_element(self, name, after, at=None): node = self.get_element(name, root=at) if node is None: # TODO: what if nodes in the path don't exist? - node = self.make_element(name.split('.')[-1]) + node = self.make_element(name.split('.')[-1], attribs) after.addnext(node) return node @@ -123,8 +136,9 @@ def get_element(self, name, root=None): return None return node - def make_element(self, elem): - return getattr(self.maker, elem)() + def make_element(self, elem, attribs=None): + attribs = attribs or {} + return getattr(self.maker, elem)(**attribs) class StructuredDocument(AkomaNtosoDocument): diff --git a/cobalt/hierarchical.py b/cobalt/hierarchical.py index c39336f..a36511b 100644 --- a/cobalt/hierarchical.py +++ b/cobalt/hierarchical.py @@ -1,6 +1,4 @@ -from iso8601 import parse_date - -from .akn import StructuredDocument, datestring +from .akn import StructuredDocument, datestring, NULL_DATE, parsedate class HierarchicalStructure(StructuredDocument): @@ -33,7 +31,7 @@ def publication_name(self): @publication_name.setter def publication_name(self, value): value = value or "" - pub = self.ensure_element('meta.publication', after=self.meta.identification) + pub = self.ensure_publication() pub.set('name', value) pub.set('showAs', value) @@ -43,13 +41,12 @@ def publication_date(self): """ pub = self.get_element('meta.publication') if pub is not None and pub.get('date'): - return parse_date(pub.get('date')).date() + return parsedate(pub.get('date')) return None @publication_date.setter def publication_date(self, value): - self.ensure_element('meta.publication', after=self.meta.identification)\ - .set('date', datestring(value)) + self.ensure_publication().set('date', datestring(value)) @property def publication_number(self): @@ -60,15 +57,14 @@ def publication_number(self): @publication_number.setter def publication_number(self, value): - self.ensure_element('meta.publication', after=self.meta.identification)\ - .set('number', value or "") + self.ensure_publication().set('number', value or "") @property def amendments(self): amendments = [] for e in self.meta.iterfind(f'.//{{{self.namespace}}}lifecycle/{{{self.namespace}}}eventRef[@type="amendment"]'): - date = parse_date(e.get('date')).date() + date = parsedate(e.get('date')) event = AmendmentEvent(date=date) amendments.append(event) @@ -132,7 +128,7 @@ def amendments(self, value): def repeal(self): e = self.meta.find(f'.//{{{self.namespace}}}lifecycle/{{{self.namespace}}}eventRef[@type="repeal"]') if e is not None: - date = parse_date(e.get('date')).date() + date = parsedate(e.get('date')) event = RepealEvent(date=date) id = e.get('source')[1:] @@ -183,6 +179,10 @@ def repeal(self, value): except AttributeError: pass + def ensure_publication(self): + return self.ensure_element('meta.publication', after=self.meta.identification, + attribs={'showAs': '', 'name': '', 'date': NULL_DATE}) + class AmendmentEvent(object): """ An event that amended a document. diff --git a/tests/test_act.py b/tests/test_act.py index a70d055..4f32e5c 100644 --- a/tests/test_act.py +++ b/tests/test_act.py @@ -22,14 +22,21 @@ def test_empty_act(self): def test_empty_body(self): a = Act() assert_not_equal(a.body.text, '') + assert_validates(a) def test_publication_date(self): a = Act() assert_is_none(a.publication_date) + assert_validates(a) a.publication_date = '2012-01-02' assert_equal(datestring(a.publication_date), '2012-01-02') assert_is_instance(a.publication_date, date) + assert_validates(a) + + a.publication_date = None + assert_is_none(a.publication_date) + assert_validates(a) def test_publication_number(self): a = Act() @@ -37,6 +44,7 @@ def test_publication_number(self): a.publication_number = '1234' assert_equal(a.publication_number, '1234') + assert_validates(a) def test_publication_name(self): a = Act() @@ -44,11 +52,13 @@ def test_publication_name(self): a.publication_name = 'Publication' assert_equal(a.publication_name, 'Publication') + assert_validates(a) def test_set_amendments(self): a = Act() a.frbr_uri = "/akn/za/act/1900/1" a.amendments = [AmendmentEvent(date='2012-02-01', amending_uri='/za/act/1980/10', amending_title="Foo")] + assert_validates(a) xml = a.to_xml(encoding='unicode', pretty_print=True) xml = xml.replace(datestring(date.today()), 'TODAY') @@ -104,6 +114,7 @@ def test_set_amendments(self): AmendmentEvent(date='2012-02-01', amending_uri='/za/act/1980/22', amending_title="Corrected"), AmendmentEvent(date='2013-03-03', amending_uri='/za/act/1990/5', amending_title="Bar"), ] + assert_validates(a) xml = a.to_xml(encoding='unicode', pretty_print=True) xml = xml.replace(datestring(date.today()), 'TODAY') @@ -171,6 +182,7 @@ def test_set_amendments(self): # clear them a.amendments = [] + assert_validates(a) xml = a.to_xml(encoding='unicode', pretty_print=True) xml = xml.replace(datestring(date.today()), 'TODAY')