Repository: jazzband/tablib
Pull request #594 (2024-07-09 01:58:16)

feat: ods: add style to datetime, date and time values 
This solves #210 but for the ods format.

Let me know if you agree with this approach in general. I will then apply the same for datetime and time types.

Code is inspired by https://stackoverflow.com/a/28577394
I tested it with LibreOffice 7.3 so far.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/jazzband/tablib/pull/594?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 92.68%. Comparing base [(`9c82f70`)](https://app.codecov.io/gh/jazzband/tablib/commit/9c82f7080ec92eec178f526882e38d17f13ba4ea?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband) to head [(`8189db6`)](https://app.codecov.io/gh/jazzband/tablib/commit/8189db69e74ee524020171d5406b8db02a61932f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband).
> Report is 2 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #594      +/-   ##
==========================================
+ Coverage   92.66%   92.68%   +0.01%     
==========================================
  Files          28       28              
  Lines        2958     2966       +8     
==========================================
+ Hits         2741     2749       +8     
  Misses        217      217              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/jazzband/tablib/pull/594?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=jazzband).

pascalfree: I changed the title and added tests.

The tests currently fail, because the datetime export has a bug that was previously covered by the import: #595
claudep: Please rebase after merge of #595

Patch:
diff --git a/src/tablib/formats/_ods.py b/src/tablib/formats/_ods.py
index cb5fbda..adab7e1 100644
--- a/src/tablib/formats/_ods.py
+++ b/src/tablib/formats/_ods.py
@@ -5,7 +5,7 @@ import datetime as dt
 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 @@ bold.addElement(style.TextProperties(
 ))
 
 
+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 @@ class ODSFormat:
 
         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 @@ class ODSFormat:
                     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:


Test patch:
diff --git a/tests/test_tablib.py b/tests/test_tablib.py
index aad6ae3..0df8e5d 100755
--- a/tests/test_tablib.py
+++ b/tests/test_tablib.py
@@ -1168,6 +1168,16 @@ class TSVTests(BaseTestCase):
 
 
 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 @@ class ODSTests(BaseTestCase):
         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: