From 1c00f21eebd915bc22ea370b1ab1e34fd50175a6 Mon Sep 17 00:00:00 2001
From: Michel Succar Medina <53895656+micsucmed@users.noreply.github.com>
Date: Fri, 8 Dec 2023 12:16:27 +0100
Subject: [PATCH] Use CSV writer for answer exports (#456)

---
 newdle/export.py          |  20 +++++++++++++++++---
 tests/api_test.py         |   4 ++--
 tests/export/answers.csv  |   4 ++--
 tests/export/answers.xlsx | Bin 5468 -> 5476 bytes
 4 files changed, 21 insertions(+), 7 deletions(-)

diff --git a/newdle/export.py b/newdle/export.py
index 2b50d7a0..1a7b0ca4 100644
--- a/newdle/export.py
+++ b/newdle/export.py
@@ -1,5 +1,7 @@
+import csv
 import datetime
-from io import BytesIO
+from contextlib import contextmanager
+from io import BytesIO, TextIOWrapper
 
 from xlsxwriter import Workbook
 
@@ -25,12 +27,24 @@ def _generate_answers_for_export(newdle):
     return rows
 
 
+@contextmanager
+def csv_text_io_wrapper(buf):
+    """IO wrapper to use the csv reader/writer on a byte stream."""
+    w = TextIOWrapper(buf, encoding='utf-8-sig', newline='')
+    try:
+        yield w
+    finally:
+        w.detach()
+
+
 def export_answers_to_csv(newdle):
     rows = _generate_answers_for_export(newdle)
-    csv = '\n'.join([','.join(row) for row in rows])
     buffer = BytesIO()
-    buffer.write(csv.encode('utf-8-sig'))
+    with csv_text_io_wrapper(buffer) as csvbuf:
+        writer = csv.writer(csvbuf, dialect='unix', quoting=csv.QUOTE_MINIMAL)
+        writer.writerows(rows)
     buffer.seek(0)
+
     return buffer
 
 
diff --git a/tests/api_test.py b/tests/api_test.py
index bb632243..b4ed989b 100644
--- a/tests/api_test.py
+++ b/tests/api_test.py
@@ -1374,13 +1374,13 @@ def test_answer_export(snapshot, monkeypatch, flask_client, dummy_uid):
     snapshot.snapshot_dir = Path(__file__).parent / 'export'
     p1 = Participant.query.filter_by(code='part1').first()
     p1.answers = {datetime(2019, 9, 11, 14, 0): Availability.available}
-    p1.comment = 'Available comment'
+    p1.comment = 'Hello, world'
     Participant.query.filter_by(code='part2').first().answers = {
         datetime(2019, 9, 11, 14, 0): Availability.unavailable
     }
     p3 = Participant.query.filter_by(code='part3').first()
     p3.answers = {datetime(2019, 9, 11, 14, 0): Availability.ifneedbe}
-    p3.comment = 'Comment'
+    p3.comment = 'Hello world'
 
     resp = flask_client.get(
         url_for('api.export_participants', code='dummy', format='csv'),
diff --git a/tests/export/answers.csv b/tests/export/answers.csv
index 94aedb57..e8d02088 100644
--- a/tests/export/answers.csv
+++ b/tests/export/answers.csv
@@ -1,4 +1,4 @@
 Participant name,2019-09-11T13:00,2019-09-11T14:00,2019-09-12T13:00,2019-09-12T13:30,Comment
 Albert Einstein,,unavailable,,,
-Guinea Pig,,ifneedbe,,,Comment
-Tony Stark,,available,,,Available comment
\ No newline at end of file
+Guinea Pig,,ifneedbe,,,Hello world
+Tony Stark,,available,,,"Hello, world"
diff --git a/tests/export/answers.xlsx b/tests/export/answers.xlsx
index f7a43dcff6b02df4f5b521ca20c06c9f08caf50c..a9789008f1c6cc6cd10c129a6196865538e387f2 100644
GIT binary patch
delta 750
zcmV<K0ulY(D&#7#zyk@NmthHy0RRB_lgR@te@Qo>N>UX{1P3I<c6XCWVv%?v+qCfa
z*eR{D9k_Y?mYL@zSbtX~*@MyoWk~&rM+wLTQz7$^zQ4tn*OX|(Wy(vG5YivebRAs;
z2UMTB05HTFNFCC`n0m#SP70{Fo?s2qA{nZR8(XTJ=~@9#8&OrVspl<O#f79thn4z|
ze+OonNMMV}R|V1>O%#;e*zt8CYE7dc6&7=Oh=MGn8-EqgDT{)p^M`<gZmo&oyH_Y7
zF_7B)l(=9!#7_r4+AYsX*+3gNLp>{!LdL&J^MZ#55P4y&$>Q8?%MFhLg$JS<b6s<H
zR{m;f=S<w@#_cwkRl4c-kr%MNZJGQre{P2izstBAGNxU|cF34@8TUiRyvvA(jK!J3
zY{qlY>1fbtOStR>Xk%C7rn6DgSxb1`Gi_rxy)13a_r^QUM?22j%Kk-kz}_9i9dOp`
z$y)EF<~clZl?$m!37HjnlPgNp@p(0CgLSi7P_jc~usT(R{bHb8Wg}*Y=2W;>1$_Rr
z(J!-i1%m+zYUb`a1OWg5E|UWXD1Uj76wowKTQs449FfKzz_g}VOPnro*?9YPOxmGn
zaI}B=+p?T5zr0fijEMplIvZQ9kgHG=H-&!NJdN(PN&;Mi3yg(6V$#d<CQnIJALUf&
zy-2$-CRKa%kj9}!?qwIE2kFb$nAApuI_(j~J2SEN&Uhf!stO%Tzjmfo$A9$E;p(qZ
z=84K&$~8oxidtY%3?6eMWo~}#*C#fckL-MuWt(ieu=bMAF8Sn2{^Zj$Uj^@xW!Qdj
zTa2O}iIX7lkUNG0kb|u|JLtJ1Mo?>NhC(|=thZ;07j!PD&k$Wbq)p(XdKHN8L-}|A
zY1-{@oarB4egjZT0|XQR0J9+ocMA!hmthHy0RRB_lc5uO32Nr<IRpU!04|dx6jK2r
glXw(50r!)$6hHyklk^lm0V$I+6(a`D5&!@I0EL=fEdT%j

delta 771
zcmV+e1N{8tD%>itzyk@+K3|fL0RRB^lgR@te`&f>C8>ZC!2t=e-Q8r8SR|gvHZA-;
zc1o)(mEh*_TV|e@VEt8<WCuzMlp!6DJW4<&m<pMP^zAjiTvDP9mnkn%LP)<s({*$a
z>`{H_0>BV!AazI!W9k)SIw_#ydW1Ddi)5%OZfvP?rfUT}ZA4Yce9xP+iVH~(4lDH^
ze-F$uk-!F%&kCeDm?$W@vE%DP)S5;?DlF#m5CvICug9x+N?8;%o!<rQb!$xw-@ZTz
ziGkGSr^E%@B7QjV!ESj@$_CoF8R|)q6f*u<nrGbKgUAbGO=hQV8*X?MDBKg(nCqIm
zvl_4FcFx3YUc21}vr0GpF7g7lvn`Wff5y#_G43*MhYY{V*bEtyF5_;<n06WQkTE+k
zn9X<!>Yokjw}kUvfHt-`Z#p?^I%x@)J<~RJ)yp#ZJH_;D!)f~(dN+030ehDecR;__
zleL~p&2xC<Di>0d5;80DMhi;R;dM1@gLSi-QL;s2usT+S{b8V7WfNwI=2*B#34Hpq
z(GO5d0|b-Y6B@IX1!Vyao}H#x`~Uy|7y<wQ6abSy6c&>n2MvE6k^-6rYKsusc12qE
z0B&nqi^k~^7mc@H_jM^6JK8_}Wm`_S@2*p443Ru%I+<IokSkvkH<^Ado@NhPMFFm%
z^Ng9kVAR|EE{##t80DDhR;1q;6RQ>-#JTU0M>+c7K*lmOCiVfLjxD0N&Md6GHx7uk
zs{Ft*j$LUrFui{bxc_M^b)H7bQ^^kygevNRMKL%`jpV8Mv~OS7WIeO%S&|gVa%1f+
zU)}P>Klv|TUiscThb+_ft}8K!dL)j5#8Vy^&OjZM-PJ+Q12KX+P%{-eGGbj`B?`|M
z^&${XQ~7hq?ia=vO!hb9Yx56KO9KQH000080000X05Fqg6c4jN2w)2d&OTp~j{yJx
z_mim;dI_GLrda#{000=1EEH1#8k2w&IsxyKyA(hH(3AWWJ^>+<I~5}a#u5Mk008O+
BP_O_1