From d9af88752aad9cd11205cd444c237d4b26ae81aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 1 May 2020 17:39:53 +0200
Subject: [PATCH] api: include file info in list API (#42371)

---
 help/fr/api-get.page         |  7 +++--
 tests/test_api.py            |  6 ++--
 wcs/fields.py                | 18 ++++++++----
 wcs/formdata.py              | 55 +++++++++++++++++-------------------
 wcs/qommon/upload_storage.py | 21 ++++++++------
 5 files changed, 60 insertions(+), 47 deletions(-)

diff --git a/help/fr/api-get.page b/help/fr/api-get.page
index 09c717245..8bb253b59 100644
--- a/help/fr/api-get.page
+++ b/help/fr/api-get.page
@@ -238,7 +238,8 @@ Les champs de type « Fichier » sont exportés selon le schéma suivant :
         "photo": {
             "filename": "exemple.txt",
             "content_type": "text/plain",
-            "content": "Q2VjaSBuJ2VzdCBwYXMgdW4gZXhlbXBsZS4="
+            "content": "Q2VjaSBuJ2VzdCBwYXMgdW4gZXhlbXBsZS4=",
+            "url": "https://.../"
         }
     },
     (...)
@@ -333,8 +334,8 @@ l'adresse.
 </screen>
 
 <p>
-À noter que pour ne pas alourdir l'export en mode <code>full=on</code>, les
-champs de type « Fichier » ne sont pas exportés.
+À noter que pour ne pas alourdir l'export en mode <code>full=on</code>, le
+contenu des champs de type « Fichier » n'est pas exporté.
 </p>
 
 </section>
diff --git a/tests/test_api.py b/tests/test_api.py
index 1349ac5a0..53d794b47 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1633,7 +1633,8 @@ def test_user_drafts(pub, local_user):
     assert resp.json['err'] == 0
     assert 'fields' in resp.json['data'][0]
     assert resp.json['data'][0]['fields']['foobar'] == 'foo@localhost'
-    assert 'file' not in resp.json['data'][0]['fields']  # no file export in full lists
+    assert 'url' in resp.json['data'][0]['fields']['file']
+    assert 'content' not in resp.json['data'][0]['fields']['file']  # no file content in full lists
     assert resp.json['data'][0]['keywords'] == ['hello', 'world']
 
     formdef.enable_tracking_codes = False
@@ -1724,7 +1725,8 @@ def test_api_list_formdata(pub, local_user):
     assert len(resp.json) == 30
     assert 'receipt_time' in resp.json[0]
     assert 'fields' in resp.json[0]
-    assert 'file' not in resp.json[0]['fields'] # no file export in full lists
+    assert 'url' in resp.json[0]['fields']['file']
+    assert 'content' not in resp.json[0]['fields']['file']  # no file content in full lists
     assert 'user' in resp.json[0]
     assert 'evolution' in resp.json[0]
     assert len(resp.json[0]['evolution']) == 2
diff --git a/wcs/fields.py b/wcs/fields.py
index 7cc19e26e..31c642121 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -1127,6 +1127,9 @@ class FileField(WidgetField):
         t += htmltext('</div>')
         return t.getvalue()
 
+    def get_download_url(self, formdata):
+        return '%sdownload?f=%s' % (formdata.get_url(), self.id)
+
     def get_opendocument_node_value(self, value, formdata=None, **kwargs):
         show_link = True
         if value.has_redirect_url():
@@ -1134,7 +1137,7 @@ class FileField(WidgetField):
             show_link = bool(value.get_redirect_url(backoffice=is_in_backoffice))
         if show_link and formdata:
             node = ET.Element('{%s}a' % OD_NS['text'])
-            node.attrib['{%s}href' % OD_NS['xlink']] = '%sdownload?f=%s' % (formdata.get_url(), self.id)
+            node.attrib['{%s}href' % OD_NS['xlink']] = self.get_download_url(formdata)
         else:
             node = ET.Element('{%s}span' % OD_NS['text'])
         node.text = od_clean_text(force_text(value))
@@ -1143,8 +1146,13 @@ class FileField(WidgetField):
     def get_csv_value(self, value, **kwargs):
         return [str(value) if value else '']
 
-    def get_json_value(self, value):
-        out = value.get_json_value()
+    def get_json_value(self, value, formdata=None, include_file_content=True, **kwargs):
+        out = value.get_json_value(include_file_content=include_file_content)
+        if formdata:
+            if value.has_redirect_url():
+                out['url'] = value.get_redirect_url()
+            else:
+                out['url'] = self.get_download_url(formdata)
         out['field_id'] = self.id
         return out
 
@@ -1323,7 +1331,7 @@ class DateField(WidgetField):
         span.text = od_clean_text(self.get_view_value(value))
         return span
 
-    def get_json_value(self, value):
+    def get_json_value(self, value, **kwargs):
         try:
             return strftime('%Y-%m-%d', value)
         except TypeError:
@@ -2406,7 +2414,7 @@ class MapField(WidgetField):
             return None
         return value
 
-    def get_json_value(self, value):
+    def get_json_value(self, value, **kwargs):
         if not value or ';' not in value:
             return None
         lat, lon = value.split(';')
diff --git a/wcs/formdata.py b/wcs/formdata.py
index 9abc343e0..5da28429d 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -107,32 +107,6 @@ def flatten_dict(d):
             del d[k]
 
 
-def get_json_dict(fields, data, include_files=True, anonymise=False):
-    new_data = {}
-    for field in fields:
-        if anonymise and field.anonymise:
-            continue
-        if not field.varname: # exports only named fields
-            continue
-        if not include_files and isinstance(field, FileField):
-            continue
-        if data is not None:
-            value = data.get(field.id)
-            if value and hasattr(field, 'get_json_value'):
-                value = field.get_json_value(value)
-        else:
-            value = display_value = None
-        if field.store_display_value:
-            new_data[field.varname + '_raw'] = value
-            new_data[field.varname] = data.get('%s_display' % field.id)
-        else:
-            new_data[field.varname] = value
-        if field.store_structured_value:
-            if data.get('%s_structured' % field.id):
-                new_data[field.varname + '_structured'] = data.get('%s_structured' % field.id)
-    return new_data
-
-
 class Evolution(object):
     who = None
     status = None
@@ -1017,6 +991,29 @@ class FormData(StorableObject):
                 return field.get_json_value(self.data[field.id])
         return None
 
+    def get_json_dict(self, fields, include_files=True, anonymise=False):
+        new_data = {}
+        for field in fields:
+            if anonymise and field.anonymise:
+                continue
+            if not field.varname:  # exports only named fields
+                continue
+            if self.data is not None:
+                value = self.data.get(field.id)
+                if value and hasattr(field, 'get_json_value'):
+                    value = field.get_json_value(value, formdata=self, include_file_content=include_files)
+            else:
+                value = None
+            if field.store_display_value:
+                new_data[field.varname + '_raw'] = value
+                new_data[field.varname] = self.data.get('%s_display' % field.id)
+            else:
+                new_data[field.varname] = value
+            if field.store_structured_value:
+                if self.data.get('%s_structured' % field.id):
+                    new_data[field.varname + '_structured'] = self.data.get('%s_structured' % field.id)
+        return new_data
+
     def get_json_export_dict(self, include_files=True, anonymise=False, user=None):
         data = {}
         data['id'] = str(self.id)
@@ -1037,7 +1034,7 @@ class FormData(StorableObject):
             if user:
                 data['user'] = user.get_json_export_dict()
 
-        data['fields'] = get_json_dict(self.formdef.fields, self.data,
+        data['fields'] = self.get_json_dict(self.formdef.fields,
                 include_files=include_files, anonymise=anonymise)
 
         data['workflow'] = {}
@@ -1048,9 +1045,9 @@ class FormData(StorableObject):
         if self.workflow_data and not anonymise:
             data['workflow']['data'] = self.workflow_data
         if self.formdef.workflow.get_backoffice_fields():
-            data['workflow']['fields'] = get_json_dict(
+            data['workflow']['fields'] = self.get_json_dict(
                     self.formdef.workflow.get_backoffice_fields(),
-                    self.data, include_files=include_files, anonymise=anonymise)
+                    include_files=include_files, anonymise=anonymise)
 
         # add a roles dictionary, with workflow functions and two special
         # entries for concerned/actions roles.
diff --git a/wcs/qommon/upload_storage.py b/wcs/qommon/upload_storage.py
index e1747b10a..9b39b8ed1 100644
--- a/wcs/qommon/upload_storage.py
+++ b/wcs/qommon/upload_storage.py
@@ -78,8 +78,9 @@ class PicklableUpload(Upload):
             return base64.encodestring(content)
         return b''
 
-    def get_json_value(self):
-        return get_storage_object(getattr(self, 'storage', None)).get_json_value(self)
+    def get_json_value(self, include_file_content=True):
+        return get_storage_object(getattr(self, 'storage', None)).get_json_value(
+                self, include_file_content=include_file_content)
 
     def can_thumbnail(self):
         return get_storage_object(getattr(self, 'storage', None)).can_thumbnail(self)
@@ -134,12 +135,14 @@ class UploadStorage(object):
             upload.fp.seek(0)
             atomic_write(filepath, upload.fp, async_op=False)
 
-    def get_json_value(self, upload):
-        return {
+    def get_json_value(self, upload, include_file_content=True):
+        value = {
             'filename': upload.base_filename,
             'content_type': upload.content_type or 'application/octet-stream',
-            'content': force_text(base64.b64encode(upload.get_content())),
         }
+        if include_file_content:
+            value['content'] = force_text(base64.b64encode(upload.get_content()))
+        return value
 
     def can_thumbnail(self, upload):
         return can_thumbnail(upload.content_type)
@@ -215,14 +218,16 @@ class RemoteOpaqueUploadStorage(object):
     def save(self, upload):
         pass
 
-    def get_json_value(self, upload):
-        return {
+    def get_json_value(self, upload, include_file_content=True):
+        value = {
             'filename': upload.base_filename,
             'content_type': upload.content_type or 'application/octet-stream',
-            'content': '',
             'storage': upload.storage,
             'storage_attrs': upload.storage_attrs,
         }
+        if include_file_content:  # actually not possible
+            value['content'] = ''
+        return value
 
     def can_thumbnail(self, upload):
         return False
-- 
2.26.2

