From 1224af4f68fbe010e1f829505d44d260f1e1e435 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 26 Nov 2021 09:02:05 +0100
Subject: [PATCH] fields: add support for card data sources to computed fields
 (#58913)

---
 tests/form_pages/test_computed_field.py | 47 ++++++++++++
 wcs/fields.py                           | 15 +++-
 wcs/variables.py                        | 96 ++++++++++++++-----------
 3 files changed, 115 insertions(+), 43 deletions(-)

diff --git a/tests/form_pages/test_computed_field.py b/tests/form_pages/test_computed_field.py
index b6b3ccb99..21c86ff0c 100644
--- a/tests/form_pages/test_computed_field.py
+++ b/tests/form_pages/test_computed_field.py
@@ -5,6 +5,7 @@ import pytest
 from django.utils.timezone import make_aware
 
 from wcs import fields
+from wcs.carddef import CardDef
 from wcs.formdef import FormDef
 from wcs.qommon.substitution import CompatibilityNamesDict
 from wcs.workflows import EditableWorkflowStatusItem, Workflow
@@ -586,3 +587,49 @@ def test_computed_field_usage_in_criteria(pub):
     resp = get_app(pub).get('/test/?param=plop')
     resp = resp.forms[0].submit('submit')  # -> 2nd page
     assert 'count with this value: 0' in resp.text
+
+
+def test_computed_field_with_data_source(pub):
+    CardDef.wipe()
+    FormDef.wipe()
+
+    carddef = CardDef()
+    carddef.name = 'items'
+    carddef.digest_templates = {'default': '{{form_var_name}}'}
+    carddef.fields = [
+        fields.StringField(id='0', label='string', varname='name'),
+        fields.StringField(id='1', label='string', varname='attr'),
+    ]
+    carddef.store()
+    for i, value in enumerate(['foo', 'bar', 'baz']):
+        carddata = carddef.data_class()()
+        carddata.data = {
+            '0': value,
+            '1': 'attr%s' % i,
+        }
+        carddata.just_created()
+        carddata.store()
+
+    ds = {'type': 'carddef:%s' % carddef.url_name}
+
+    formdef = FormDef()
+    formdef.name = 'test'
+    formdef.fields = [
+        fields.ComputedField(
+            id='1',
+            label='computed',
+            varname='computed',
+            value_template='{{ request.GET.param }}',
+            freeze_on_initial_value=True,
+            data_source=ds,
+        ),
+        fields.CommentField(id='2', label='X{{ form_var_computed_live_var_name }}Y', type='comment'),
+    ]
+    formdef.store()
+    formdef.data_class().wipe()
+
+    resp = get_app(pub).get('/test/?param=%s' % carddata.id)
+    assert 'XbazY' in resp.text
+
+    resp = get_app(pub).get('/test/?param=%s' % 'invalid')
+    assert 'XY' in resp.text
diff --git a/wcs/fields.py b/wcs/fields.py
index 6f661df70..3ddd95edd 100644
--- a/wcs/fields.py
+++ b/wcs/fields.py
@@ -3484,6 +3484,7 @@ class ComputedField(Field):
 
     value_template = None
     freeze_on_initial_value = False
+    data_source = {}
 
     add_to_form = None
     add_to_view_form = None
@@ -3497,7 +3498,7 @@ class ComputedField(Field):
     def get_admin_attributes(self):
         attributes = super().get_admin_attributes()
         attributes.remove('condition')
-        return attributes + ['varname', 'value_template', 'freeze_on_initial_value']
+        return attributes + ['varname', 'value_template', 'freeze_on_initial_value', 'data_source']
 
     def fill_admin_form(self, form):
         form.add(StringWidget, 'label', title=_('Label'), value=self.label, required=True, size=50)
@@ -3526,6 +3527,18 @@ class ComputedField(Field):
             title=_('Freeze on initial value'),
             value=self.freeze_on_initial_value,
         )
+        form.add(
+            data_sources.DataSourceSelectionWidget,
+            'data_source',
+            value=self.data_source,
+            allow_jsonp=False,
+            title=_('Data Source'),
+            hint=_('This will make linked card data available for expressions.'),
+            required=False,
+        )
+
+    def get_real_data_source(self):
+        return data_sources.get_real(self.data_source)
 
 
 register_field_class(ComputedField)
diff --git a/wcs/variables.py b/wcs/variables.py
index 1c7bea9a6..87cdccd47 100644
--- a/wcs/variables.py
+++ b/wcs/variables.py
@@ -844,12 +844,64 @@ class LazyFieldVarComplex(LazyFieldVar):
         raise KeyError(key)
 
 
-class LazyFieldVarComputed(LazyFieldVarComplex):
+class LazyFieldVarLiveCardMixin:
+    @property
+    def live(self):
+        real_data_source = self._field.get_real_data_source()
+        if not (
+            real_data_source
+            and (
+                real_data_source.get('type', '').startswith('carddef:')
+                or real_data_source.get('type', '') == 'wcs:users'
+            )
+        ):
+            raise AttributeError('live')
+        if real_data_source.get('type', '') == 'wcs:users':
+            return LazyUser(get_publisher().user_class.get(self._data.get(self._field.id)))
+        request = get_request()
+        card_id = self._data.get(self._field.id)
+        if request:
+            # cache during request
+            cache_key = '%s-%s' % (self._field.data_source['type'], card_id)
+            if not hasattr(request, 'live_card_cache'):
+                request.live_card_cache = {}
+            else:
+                carddata = request.live_card_cache.get(cache_key)
+                if carddata is not None:
+                    # cached data
+                    if isinstance(carddata, Exception):
+                        raise carddata
+                    return LazyFormData(carddata)
+        from wcs.carddef import CardDef
+
+        try:
+            carddef = CardDef.get_by_urlname(self._field.data_source['type'].split(':')[1])
+            carddata = carddef.data_class().get(card_id)
+            if request:
+                request.live_card_cache[cache_key] = carddata
+        except KeyError:
+            if request:
+                request.live_card_cache[cache_key] = AttributeError('live')
+            raise AttributeError('live')
+        return LazyFormData(carddata)
+
+
+class LazyFieldVarComputed(LazyFieldVarComplex, LazyFieldVarLiveCardMixin):
+    def inspect_keys(self):
+        keys = super().inspect_keys()
+        try:
+            self.live
+        except AttributeError:
+            pass  # don't advertise if there's no value behind
+        else:
+            keys.append('live')
+        return keys
+
     def get_field_var_value(self):
         return self.get_value()
 
 
-class LazyFieldVarStructured(LazyFieldVarComplex):
+class LazyFieldVarStructured(LazyFieldVarComplex, LazyFieldVarLiveCardMixin):
     def inspect_keys(self):
         if not self._data.get(self._field.id):
             return []
@@ -887,46 +939,6 @@ class LazyFieldVarStructured(LazyFieldVarComplex):
     def get_field_var_value(self):
         return self.structured
 
-    @property
-    def live(self):
-        real_data_source = self._field.get_real_data_source()
-        if not (
-            real_data_source
-            and (
-                real_data_source.get('type', '').startswith('carddef:')
-                or real_data_source.get('type', '') == 'wcs:users'
-            )
-        ):
-            raise AttributeError('live')
-        if real_data_source.get('type', '') == 'wcs:users':
-            return LazyUser(get_publisher().user_class.get(self._data.get(self._field.id)))
-        request = get_request()
-        card_id = self._data.get(self._field.id)
-        if request:
-            # cache during request
-            cache_key = '%s-%s' % (self._field.data_source['type'], card_id)
-            if not hasattr(request, 'live_card_cache'):
-                request.live_card_cache = {}
-            else:
-                carddata = request.live_card_cache.get(cache_key)
-                if carddata is not None:
-                    # cached data
-                    if isinstance(carddata, Exception):
-                        raise carddata
-                    return LazyFormData(carddata)
-        from wcs.carddef import CardDef
-
-        try:
-            carddef = CardDef.get_by_urlname(self._field.data_source['type'].split(':')[1])
-            carddata = carddef.data_class().get(card_id)
-            if request:
-                request.live_card_cache[cache_key] = carddata
-        except KeyError:
-            if request:
-                request.live_card_cache[cache_key] = AttributeError('live')
-            raise AttributeError('live')
-        return LazyFormData(carddata)
-
 
 class DateOperatorsMixin:
     def __eq__(self, other):
-- 
2.34.0

