diff --git a/src/tablib/formats/_ods.py b/src/tablib/formats/_ods.py index cb5fbdab..adab7e18 100644 --- a/src/tablib/formats/_ods.py +++ b/src/tablib/formats/_ods.py @@ -5,7 +5,7 @@ import numbers from io import BytesIO -from odf import opendocument, style, table, text +from odf import number, opendocument, style, table, text import tablib @@ -17,6 +17,52 @@ )) +def set_date_style(style): + style.addElement(number.Year(style="long")) + style.addElement(number.Text(text="-")) + style.addElement(number.Month(style="long")) + style.addElement(number.Text(text="-")) + style.addElement(number.Day(style="long")) + + +def set_time_style(style): + style.addElement(number.Hours(style="long")) + style.addElement(number.Text(text=":")) + style.addElement(number.Minutes(style="long")) + style.addElement(number.Text(text=":")) + style.addElement(number.Seconds(style="long", decimalplaces="0")) + + +date_style = number.DateStyle(name="date-style1") +set_date_style(date_style) +ds = style.Style( + name="ds1", + datastylename="date-style1", + parentstylename="Default", + family="table-cell", +) + +time_style = number.DateStyle(name="time-style1") +set_time_style(time_style) +ts = style.Style( + name="ts1", + datastylename="time-style1", + parentstylename="Default", + family="table-cell", +) + +datetime_style = number.DateStyle(name="datetime-style1") +set_date_style(datetime_style) +datetime_style.addElement(number.Text(text=" ")) +set_time_style(datetime_style) +dts = style.Style( + name="dts1", + datastylename="datetime-style1", + parentstylename="Default", + family="table-cell", +) + + class ODSFormat: title = 'ods' extensions = ('ods',) @@ -27,6 +73,12 @@ def export_set(cls, dataset): wb = opendocument.OpenDocumentSpreadsheet() wb.automaticstyles.addElement(bold) + wb.styles.addElement(date_style) + wb.automaticstyles.addElement(ds) + wb.styles.addElement(time_style) + wb.automaticstyles.addElement(ts) + wb.styles.addElement(datetime_style) + wb.automaticstyles.addElement(dts) ws = table.Table(name=dataset.title if dataset.title else 'Tablib Dataset') wb.spreadsheet.addElement(ws) @@ -162,12 +214,22 @@ def dset_sheet(cls, dataset, ws): cell = table.TableCell(valuetype="float", value=col) elif isinstance(col, dt.datetime): cell = table.TableCell( - valuetype="date", datevalue=col.strftime('%Y-%m-%dT%H:%M:%S') + valuetype="date", + datevalue=col.strftime('%Y-%m-%dT%H:%M:%S'), + stylename=dts, ) + cell.addElement(text.P(text=col.strftime('%Y-%m-%d %H:%M:%S'))) elif isinstance(col, dt.date): - cell = table.TableCell(valuetype="date", datevalue=col.strftime('%Y-%m-%d')) + date_value = col.strftime('%Y-%m-%d') + cell = table.TableCell(valuetype="date", datevalue=date_value, stylename=ds) + cell.addElement(text.P(text=date_value)) elif isinstance(col, dt.time): - cell = table.TableCell(valuetype="time", timevalue=col.strftime('PT%HH%MM%SS')) + cell = table.TableCell( + valuetype="time", + timevalue=col.strftime('PT%HH%MM%SS'), + stylename=ts, + ) + cell.addElement(text.P(text=col.strftime('%H:%M:%S'))) elif col is None: cell = table.TableCell(valuetype="void") else: diff --git a/tests/test_tablib.py b/tests/test_tablib.py index aad6ae3c..0df8e5d0 100755 --- a/tests/test_tablib.py +++ b/tests/test_tablib.py @@ -1168,6 +1168,16 @@ def test_tsv_export(self): class ODSTests(BaseTestCase): + FORMAT_CONVERT = { + 'yearlong': '%Y', + 'monthlong': '%m', + 'daylong': '%d', + 'hourslong': '%H', + 'minuteslong': '%M', + 'secondslong': '%S', + 'secondslong0': '%S', + } + def test_ods_export_import_set(self): date = dt.date(2019, 10, 4) date_time = dt.datetime(2019, 10, 4, 12, 30, 8) @@ -1188,6 +1198,39 @@ def test_ods_export_import_set(self): self.assertEqual(data.dict[0]['date/time'], date_time) self.assertEqual(data.dict[0]['None'], '') + def test_ods_export_display(self): + """Test that exported datetime types are displayed correctly in office software""" + date = dt.date(2019, 10, 4) + date_time = dt.datetime(2019, 10, 4, 12, 30, 8) + time = dt.time(14, 30) + data.append((date, time, date_time)) + data.headers = ('date', 'time', 'date/time') + _ods = data.ods + ods_book = opendocument.load(BytesIO(_ods)) + styles = {style.getAttribute('name'): style for style in ods_book.styles.childNodes} + automatic_styles = { + style.getAttribute('name'): style.getAttribute('datastylename') + for style in ods_book.automaticstyles.childNodes + } + + def get_format(cell): + style = styles[automatic_styles[cell.getAttribute('stylename')]] + f = [] + for number in style.childNodes: + name = number.qname[1] + ''.join(number.attributes.values()) + f.append(self.FORMAT_CONVERT.get(name, str(number))) + return ''.join(f) + + cells = ods_book.spreadsheet.getElementsByType(table.TableRow)[1].childNodes + self.assertEqual(str(date), str(cells[0])) + self.assertEqual('%Y-%m-%d', get_format(cells[0])) + + self.assertEqual(str(time), str(cells[1])) + self.assertEqual('%H:%M:%S', get_format(cells[1])) + + self.assertEqual(str(date_time), str(cells[2])) + self.assertEqual('%Y-%m-%d %H:%M:%S', get_format(cells[2])) + def test_ods_import_book(self): ods_source = Path(__file__).parent / 'files' / 'book.ods' with ods_source.open('rb') as fh: