From 779e697cf9208152c24b74e5c961096567d9d7bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 22 Jun 2021 14:48:47 +0200
Subject: [PATCH 1/4] formdef: new digest_templates field (#45633)

---
 tests/admin_pages/test_card.py             | 14 +++++-----
 tests/admin_pages/test_datasource.py       |  2 +-
 tests/admin_pages/test_form.py             |  6 ++---
 tests/api/test_carddef.py                  |  8 ++++--
 tests/api/test_custom_view.py              |  2 +-
 tests/api/test_formdata.py                 |  4 ++-
 tests/api/test_user.py                     |  2 +-
 tests/backoffice_pages/test_all.py         |  6 ++---
 tests/backoffice_pages/test_carddata.py    |  6 ++---
 tests/backoffice_pages/test_custom_view.py |  2 +-
 tests/form_pages/test_all.py               | 12 ++++-----
 tests/test_carddef.py                      |  2 +-
 tests/test_formdata.py                     | 12 ++++-----
 tests/test_formdef_import.py               |  6 ++---
 tests/test_workflows.py                    |  6 ++---
 wcs/admin/forms.py                         | 21 ++++++++++-----
 wcs/backoffice/cards.py                    |  2 +-
 wcs/backoffice/management.py               |  2 +-
 wcs/carddef.py                             |  2 +-
 wcs/formdata.py                            |  2 +-
 wcs/formdef.py                             | 31 ++++++++++++++++++++--
 21 files changed, 96 insertions(+), 54 deletions(-)

diff --git a/tests/admin_pages/test_card.py b/tests/admin_pages/test_card.py
index 55397123..ce9a2530 100644
--- a/tests/admin_pages/test_card.py
+++ b/tests/admin_pages/test_card.py
@@ -215,7 +215,7 @@ def test_card_templates(pub):
     resp = resp.follow()
     assert_option_display(resp, 'Templates', 'Custom')
     carddef = CardDef.get(carddef.id)
-    assert carddef.digest_template == 'X{{form_var_test}}Y'
+    assert carddef.digest_templates['default'] == 'X{{form_var_test}}Y'
     assert carddef.lateral_template is None
     assert carddef.submission_lateral_template is None
     assert 'Existing cards will be updated in the background.' in resp.text
@@ -226,7 +226,7 @@ def test_card_templates(pub):
     resp = resp.form.submit().follow()
     assert_option_display(resp, 'Templates', 'Custom')
     carddef = CardDef.get(carddef.id)
-    assert carddef.digest_template == 'X{{form_var_test}}Y'
+    assert carddef.digest_templates['default'] == 'X{{form_var_test}}Y'
     assert carddef.lateral_template == 'X{{form_var_test}}Y'
     assert carddef.submission_lateral_template == 'X{{form_var_test}}YZ'
     assert 'Existing cards will be updated in the background.' not in resp.text
@@ -241,7 +241,7 @@ def test_card_digest_template(pub):
     carddef.fields = [
         fields.StringField(id='1', label='Test', type='string', varname='foo'),
     ]
-    carddef.digest_template = 'X{{ form_var_foo }}Y'
+    carddef.digest_templates = {'default': 'X{{ form_var_foo }}Y'}
     carddef.store()
     carddata = carddef.data_class()()
     carddata.data = {'1': 'bar'}
@@ -263,14 +263,14 @@ def test_card_digest_template(pub):
     resp.form['digest_template'] = ''
     resp = resp.form.submit().follow()
     carddef = CardDef.get(carddef.id)
-    assert carddef.digest_template is None
+    assert carddef.digest_templates['default'] is None
     assert 'Existing cards will be updated in the background.' in resp.text
 
     # afterjobs are actually run synchronously during tests; we don't have
     # to wait to check the digest has been updated:
     assert carddef.data_class().get(carddata.id).digest == 'XbarY'
 
-    carddef.digest_template = '{{ form_var_foo }}'
+    carddef.digest_templates = {'default': '{{ form_var_foo }}'}
     carddef.store()
 
     # a formdef using the carddef as datasource
@@ -286,7 +286,7 @@ def test_card_digest_template(pub):
     resp = resp.form.submit()
     assert 'Can not empty digest template: this card model is used as data source in some forms.' in resp.text
     carddef = CardDef.get(carddef.id)
-    assert carddef.digest_template is not None
+    assert carddef.digest_templates['default'] is not None
 
     # error: not stored, and no after jobs
     resp = app.get('/backoffice/cards/%s/' % carddef.id)
@@ -369,7 +369,7 @@ def test_card_custom_view_data_source(pub):
     ]
     carddef.backoffice_submission_roles = user.roles
     carddef.workflow_roles = {'_editor': user.roles[0]}
-    carddef.digest_template = '{{ form_var_foo }}'
+    carddef.digest_templates = {'default': '{{ form_var_foo }}'}
     carddef.store()
     carddef.data_class().wipe()
 
diff --git a/tests/admin_pages/test_datasource.py b/tests/admin_pages/test_datasource.py
index 09756b37..8091f9a7 100644
--- a/tests/admin_pages/test_datasource.py
+++ b/tests/admin_pages/test_datasource.py
@@ -72,7 +72,7 @@ def test_data_sources_from_carddefs(pub):
     carddef = CardDef()
     carddef.name = 'foo'
     carddef.fields = []
-    carddef.digest_template = 'foo bar'
+    carddef.digest_templates = {'default': 'foo bar'}
     carddef.store()
 
     custom_view = pub.custom_view_class()
diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py
index 3070976b..7af915bf 100644
--- a/tests/admin_pages/test_form.py
+++ b/tests/admin_pages/test_form.py
@@ -901,7 +901,7 @@ def test_form_templates(pub):
     resp = resp.follow()
     assert_option_display(resp, 'Templates', 'Custom')
     formdef = FormDef.get(formdef.id)
-    assert formdef.digest_template == 'X{{form_var_test}}Y'
+    assert formdef.digest_templates['default'] == 'X{{form_var_test}}Y'
     assert formdef.lateral_template is None
     assert formdef.submission_lateral_template is None
 
@@ -916,7 +916,7 @@ def test_form_templates(pub):
     resp = resp.form.submit().follow()
     assert_option_display(resp, 'Templates', 'Custom')
     formdef = FormDef.get(formdef.id)
-    assert formdef.digest_template == 'X{{form_var_test}}Y'
+    assert formdef.digest_templates['default'] == 'X{{form_var_test}}Y'
     assert formdef.lateral_template == 'X{{form_var_test}}Y'
     assert formdef.submission_lateral_template == 'X{{form_var_test}}YZ'
     assert 'Existing forms will be updated in the background.' not in resp.text
@@ -1699,7 +1699,7 @@ def test_form_edit_item_field_data_source(pub):
         (u'python', False, u'Python Expression'),
     ]
 
-    carddef.digest_template = 'plop'
+    carddef.digest_templates = {'default': 'plop'}
     carddef.store()
 
     resp = app.get('/backoffice/forms/1/fields/1/')
diff --git a/tests/api/test_carddef.py b/tests/api/test_carddef.py
index 33c227b2..6615f700 100644
--- a/tests/api/test_carddef.py
+++ b/tests/api/test_carddef.py
@@ -84,7 +84,7 @@ def test_cards(pub, local_user):
     carddef.name = 'test'
     carddef.fields = [fields.StringField(id='0', label='foobar', varname='foo')]
     carddef.workflow_roles = {'_viewer': role.id}
-    carddef.digest_template = 'bla {{ form_var_foo }} xxx'
+    carddef.digest_templates = {'default': 'bla {{ form_var_foo }} xxx'}
     carddef.store()
 
     carddef.data_class().wipe()
@@ -177,6 +177,7 @@ def test_cards_import_csv(pub, local_user, auth):
     local_user.roles = [role.id]
     local_user.store()
 
+    ApiAccess.wipe()
     access = ApiAccess()
     access.name = 'test'
     access.access_identifier = 'test'
@@ -218,7 +219,7 @@ def test_cards_import_csv(pub, local_user, auth):
     ]
     carddef.workflow_roles = {'_viewer': role.id}
     carddef.backoffice_submission_roles = [role.id]
-    carddef.digest_template = 'bla {{ form_var_foo }} xxx'
+    carddef.digest_templates = {'default': 'bla {{ form_var_foo }} xxx'}
     carddef.store()
 
     carddef.data_class().wipe()
@@ -275,6 +276,7 @@ def test_cards_restricted_api(pub, local_user):
     formdata.just_created()
     formdata.store()
 
+    ApiAccess.wipe()
     access = ApiAccess()
     access.name = 'test'
     access.access_identifier = 'test'
@@ -326,6 +328,7 @@ def test_cards_http_auth_access(pub, local_user):
     formdata.just_created()
     formdata.store()
 
+    ApiAccess.wipe()
     access = ApiAccess()
     access.name = 'test'
     access.access_identifier = 'test'
@@ -369,6 +372,7 @@ def test_card_submit(pub, local_user, auth):
     local_user.roles = [role.id]
     local_user.store()
 
+    ApiAccess.wipe()
     access = ApiAccess()
     access.name = 'test'
     access.access_identifier = 'test'
diff --git a/tests/api/test_custom_view.py b/tests/api/test_custom_view.py
index 94a848d6..93cc4c7a 100644
--- a/tests/api/test_custom_view.py
+++ b/tests/api/test_custom_view.py
@@ -78,7 +78,7 @@ def test_api_custom_view_access(pub, local_user):
     carddef.name = 'test'
     carddef.fields = [fields.StringField(id='0', label='foobar', varname='foo')]
     carddef.workflow_roles = {'_viewer': role.id}
-    carddef.digest_template = 'bla {{ form_var_foo }} xxx'
+    carddef.digest_templates = {'default': 'bla {{ form_var_foo }} xxx'}
     carddef.geolocations = {'base': 'Location'}
     carddef.store()
 
diff --git a/tests/api/test_formdata.py b/tests/api/test_formdata.py
index d65afece..04706a2c 100644
--- a/tests/api/test_formdata.py
+++ b/tests/api/test_formdata.py
@@ -109,7 +109,7 @@ def ics_data(local_user):
         fields.StringField(id='0', label='foobar', varname='foobar'),
         fields.StringField(id='1', label='foobar2', varname='foobar2'),
     ]
-    formdef.digest_template = 'plöp {{ form_var_foobar }} plÔp'
+    formdef.digest_templates = {'default': 'plöp {{ form_var_foobar }} plÔp'}
     formdef.store()
 
     data_class = formdef.data_class()
@@ -857,6 +857,7 @@ def test_api_access_restrict_to_anonymised_data(pub, local_user, auth):
         formdata.store()
 
     # check normal API behaviour: get all data
+    ApiAccess.wipe()
     access = ApiAccess()
     access.name = 'test'
     access.access_identifier = 'test'
@@ -1166,6 +1167,7 @@ def test_api_global_listing(pub, local_user, user, auth):
     app = get_app(pub)
 
     if user == 'api-access':
+        ApiAccess.wipe()
         access = ApiAccess()
         access.name = 'test'
         access.access_identifier = 'test'
diff --git a/tests/api/test_user.py b/tests/api/test_user.py
index 2f02fdac..44bc760b 100644
--- a/tests/api/test_user.py
+++ b/tests/api/test_user.py
@@ -289,7 +289,7 @@ def test_user_forms(pub, local_user):
     assert len(resp.json['data']) == 1
 
     # check digest is part of contents
-    formdef.digest_template = 'XYZ'
+    formdef.digest_templates = {'default': 'XYZ'}
     formdef.data_class().get(formdata.id).store()
     assert formdef.data_class().get(formdata.id).digest == 'XYZ'
     resp = get_app(pub).get(sign_uri('/api/user/forms', user=local_user))
diff --git a/tests/backoffice_pages/test_all.py b/tests/backoffice_pages/test_all.py
index f652e9e2..e3e5b498 100644
--- a/tests/backoffice_pages/test_all.py
+++ b/tests/backoffice_pages/test_all.py
@@ -765,7 +765,7 @@ def test_backoffice_card_field_columns(pub):
     ]
     carddef.backoffice_submission_roles = user.roles
     carddef.workflow_roles = {'_editor': user.roles[0]}
-    carddef.digest_template = 'card {{form_var_foo}}'
+    carddef.digest_templates = {'default': 'card {{ form_var_foo }}'}
     carddef.store()
     carddef.data_class().wipe()
 
@@ -3056,7 +3056,7 @@ def test_global_listing(pub):
     formdata = formdef.data_class().get(
         re.findall(r'data-link="(.*?)"', app.get('/backoffice/management/listing').text)[0].split('/')[-2]
     )
-    formdata.formdef.digest_template = 'digest of number <{{form_number}}>'
+    formdata.formdef.digest_templates = {'default': 'digest of number <{{form_number}}>'}
     formdata.store()
     assert formdata.get(formdata.id).digest
     resp = app.get('/backoffice/management/listing')
@@ -3522,7 +3522,7 @@ def test_backoffice_sidebar_user_context(pub):
     # check there's nothing in the sidebar
     assert '/user-pending-forms' not in resp.text
 
-    number31.formdef.digest_template = 'digest of number {{form_number}}'
+    number31.formdef.digest_templates = {'default': 'digest of number {{form_number}}'}
     number31.user_id = user.id
     number31.store()
     resp = app.get('/backoffice/management/form-title/%s/' % number31.id)
diff --git a/tests/backoffice_pages/test_carddata.py b/tests/backoffice_pages/test_carddata.py
index 5ac7d8d4..f111085b 100644
--- a/tests/backoffice_pages/test_carddata.py
+++ b/tests/backoffice_pages/test_carddata.py
@@ -213,7 +213,7 @@ def test_studio_card_item_link(pub):
     ]
     carddef.backoffice_submission_roles = user.roles
     carddef.workflow_roles = {'_editor': user.roles[0]}
-    carddef.digest_template = 'card {{form_var_foo}}'
+    carddef.digest_templates = {'default': 'card {{ form_var_foo }}'}
     carddef.store()
     carddef.data_class().wipe()
 
@@ -519,7 +519,7 @@ def test_backoffice_cards_wscall_failure_display(http_requests, pub):
     carddef.backoffice_submission_roles = user.roles
     carddef.workflow_id = workflow.id
     carddef.workflow_roles = {'_editor': user.roles[0]}
-    carddef.digest_template = 'card {{form_var_foo}}'
+    carddef.digest_templates = {'default': 'card {{ form_var_foo }}'}
     carddef.store()
     carddef.data_class().wipe()
 
@@ -550,7 +550,7 @@ def test_block_card_item_link(pub, blocks_feature):
     ]
     carddef.backoffice_submission_roles = user.roles
     carddef.workflow_roles = {'_editor': user.roles[0]}
-    carddef.digest_template = 'card {{form_var_foo}}'
+    carddef.digest_templates = {'default': 'card {{ form_var_foo }}'}
     carddef.store()
     carddef.data_class().wipe()
 
diff --git a/tests/backoffice_pages/test_custom_view.py b/tests/backoffice_pages/test_custom_view.py
index 0f8784e3..ac088b28 100644
--- a/tests/backoffice_pages/test_custom_view.py
+++ b/tests/backoffice_pages/test_custom_view.py
@@ -767,7 +767,7 @@ def test_carddata_custom_view_is_default(pub):
         fields.StringField(id='1', label='Test', type='string', varname='foo'),
     ]
     carddef.backoffice_submission_roles = user.roles
-    carddef.digest_template = '{{ form_var_foo }}'
+    carddef.digest_templates = {'default': '{{ form_var_foo }}'}
     carddef.store()
     carddef.data_class().wipe()
 
diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index eb9b49d2..fa274546 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -5312,7 +5312,7 @@ def test_item_field_from_cards(pub):
 
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_name}}'
+    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'),
@@ -5364,7 +5364,7 @@ def test_item_field_from_custom_view_on_cards(pub):
     CardDef.wipe()
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_attr}}'
+    carddef.digest_templates = {'default': '{{form_var_name}}'}
     carddef.workflow_roles = {'_editor': user.roles[0]}
     carddef.fields = [
         fields.ItemField(id='0', type='item', label='item', varname='item', items=['foo', 'bar', 'baz']),
@@ -5443,7 +5443,7 @@ def test_dynamic_item_field_from_custom_view_on_cards(pub):
     CardDef.wipe()
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_attr}}'
+    carddef.digest_templates = {'default': '{{form_var_attr}}'}
     carddef.workflow_roles = {'_editor': user.roles[0]}
     carddef.fields = [
         fields.ItemField(id='0', type='item', label='item', varname='item', items=['foo', 'bar', 'baz']),
@@ -5557,7 +5557,7 @@ def test_item_field_from_cards_check_lazy_live(pub):
     CardDef.wipe()
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_name}}'
+    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'),
@@ -6145,7 +6145,7 @@ def test_item_field_autocomplete_cards_source(pub):
 
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_name}}'
+    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'),
@@ -7992,7 +7992,7 @@ def test_backoffice_fields_just_after_conditional_form_edit_action(pub):
 def test_backoffice_fields_set_from_live(pub):
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_name}}'
+    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'),
diff --git a/tests/test_carddef.py b/tests/test_carddef.py
index a7eb6c7a..d24effd4 100644
--- a/tests/test_carddef.py
+++ b/tests/test_carddef.py
@@ -338,7 +338,7 @@ def test_data_source_access_by_id(pub):
     carddef.fields = [
         StringField(id='1', label='Test', type='string', varname='foo'),
     ]
-    carddef.digest_template = '{{ form_var_foo }}'
+    carddef.digest_templates = {'default': '{{ form_var_foo }}'}
     carddef.store()
     carddef.data_class().wipe()
 
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index 896b3dac..9c326968 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -934,7 +934,7 @@ def test_lazy_formdata_live_item(pub):
     CardDef.wipe()
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_name}}'
+    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'),
@@ -2304,7 +2304,7 @@ def test_form_digest_date(pub):
     formdef.name = 'foobar'
     formdef.url_name = 'foobar'
     formdef.fields = [fields.DateField(id='0', label='date', varname='date')]
-    formdef.digest_template = 'plop {{ form_var_date }} plop'
+    formdef.digest_templates = {'default': 'plop {{ form_var_date }} plop'}
     formdef.store()
 
     formdata = formdef.data_class()()
@@ -2319,21 +2319,21 @@ def test_form_digest_date(pub):
     formdata.store()
     assert formdef.data_class().get(formdata.id).digest == 'plop 12/05/2015 plop'
 
-    formdef.digest_template = 'plop {{ form_var_date|date:"Y" }} plop'
+    formdef.digest_templates = {'default': 'plop {{ form_var_date|date:"Y" }} plop'}
     formdef.store()
     formdata = formdef.data_class()()
     formdata.data = {'0': time.strptime('2015-05-12', '%Y-%m-%d')}
     formdata.store()
     assert formdef.data_class().get(formdata.id).digest == 'plop 2015 plop'
 
-    formdef.digest_template = 'plop {{ form_var_date_raw|date:"Y" }} plop'
+    formdef.digest_templates = {'default': 'plop {{ form_var_date_raw|date:"Y" }} plop'}
     formdef.store()
     formdata = formdef.data_class()()
     formdata.data = {'0': time.strptime('2015-05-12', '%Y-%m-%d')}
     formdata.store()
     assert formdef.data_class().get(formdata.id).digest == 'plop 2015 plop'
 
-    formdef.digest_template = 'plop {{ form_var_date|date:"Y" }} plop'
+    formdef.digest_templates = {'default': 'plop {{ form_var_date|date:"Y" }} plop'}
     formdef.store()
     formdata = formdef.data_class()()
     formdata.data = {'0': None}
@@ -2341,7 +2341,7 @@ def test_form_digest_date(pub):
     assert formdef.data_class().get(formdata.id).digest == 'plop  plop'
 
     # check there's no crash when an invaliad variable is given
-    formdef.digest_template = 'plop {{ blah|date:"Y" }} plop'
+    formdef.digest_templates = {'default': 'plop {{ blah|date:"Y" }} plop'}
     formdef.store()
     formdata = formdef.data_class()()
     formdata.data = {'0': time.strptime('2015-05-12', '%Y-%m-%d')}
diff --git a/tests/test_formdef_import.py b/tests/test_formdef_import.py
index d766b302..28580248 100644
--- a/tests/test_formdef_import.py
+++ b/tests/test_formdef_import.py
@@ -671,13 +671,13 @@ def test_field_validation():
     assert f2.fields[0].validation == {'type': 'regex', 'value': '\\d'}
 
 
-def test_digest_template():
+def test_digest_templates():
     formdef = FormDef()
     formdef.name = 'Foo'
     formdef.fields = []
-    formdef.digest_template = '{{form_number}}'
+    formdef.digest_templates = {'default': '{{form_number}}', 'custom-view:foo-bar': 'plop'}
     f2 = assert_xml_import_export_works(formdef)
-    assert f2.digest_template == formdef.digest_template
+    assert f2.digest_templates == formdef.digest_templates
 
 
 def test_field_prefill():
diff --git a/tests/test_workflows.py b/tests/test_workflows.py
index f60e78ea..8beadcf1 100644
--- a/tests/test_workflows.py
+++ b/tests/test_workflows.py
@@ -4827,7 +4827,7 @@ def test_set_backoffice_field_card_item(two_pubs):
 
     carddef = CardDef()
     carddef.name = 'items'
-    carddef.digest_template = '{{form_var_name}}'
+    carddef.digest_templates = {'default': '{{form_var_name}}'}
     carddef.fields = [
         StringField(id='0', label='string', varname='name'),
         StringField(id='1', label='string', varname='attr'),
@@ -6335,7 +6335,7 @@ def test_call_external_workflow_with_data_sourced_object(two_pubs):
     carddef.fields = [
         StringField(id='0', label='string', varname='card_string'),
     ]
-    carddef.digest_template = '{{ form_var_card_string }}'
+    carddef.digest_templates = {'default': '{{ form_var_card_string }}'}
     carddef.workflow = carddef_wf
     carddef.store()
 
@@ -6725,7 +6725,7 @@ def test_edit_carddata_with_data_sourced_object(pub):
         StringField(id='1', label='Last Name', varname='last_name'),
         ItemField(id='2', label='Profession', type='item', varname='profession', data_source=datasource),
     ]
-    carddef.digest_template = '{{ form_var_first_name }} {{ form_var_last_name }}'
+    carddef.digest_templates = {'default': '{{ form_var_first_name }} {{ form_var_last_name }}'}
     carddef.store()
 
     carddata = carddef.data_class()()
diff --git a/wcs/admin/forms.py b/wcs/admin/forms.py
index c96795fb..7257ba16 100644
--- a/wcs/admin/forms.py
+++ b/wcs/admin/forms.py
@@ -382,7 +382,11 @@ class OptionsDirectory(Directory):
     def templates(self):
         form = Form(enctype='multipart/form-data')
         form.add(
-            StringWidget, 'digest_template', title=_('Digest'), value=self.formdef.digest_template, size=50
+            StringWidget,
+            'digest_template',
+            title=_('Digest'),
+            value=self.formdef.default_digest_template,
+            size=50,
         )
         form.add(
             WysiwygTextWidget,
@@ -464,10 +468,15 @@ class OptionsDirectory(Directory):
                             self.formdef.geolocations = None
                     else:
                         new_value = widget.parse()
-                        if getattr(self.formdef, attr, None) != new_value:
-                            if attr == 'digest_template':
+                        if attr == 'digest_template':
+                            if self.formdef.default_digest_template != new_value:
                                 self.changed = True
-                            setattr(self.formdef, attr, new_value)
+                                if not self.formdef.digest_templates:
+                                    self.formdef.digest_templates = {}
+                                self.formdef.digest_templates['default'] = new_value
+                        else:
+                            if getattr(self.formdef, attr, None) != new_value:
+                                setattr(self.formdef, attr, new_value)
             if not form.has_errors():
                 self.formdef.store(comment=_('Changed "%s" parameters') % title)
                 return redirect('..')
@@ -789,7 +798,7 @@ class FormDefPage(Directory):
             )
 
         if (
-            self.formdef.digest_template
+            self.formdef.default_digest_template
             or self.formdef.lateral_template
             or self.formdef.submission_lateral_template
         ):
@@ -1914,7 +1923,7 @@ class FormsDirectory(AccessControlled, Directory):
             try:
                 formdef = self.formdef_class.import_from_xml(fp)
                 get_session().message = ('info', str(self.import_success_message))
-            except FormdefImportRecoverableError as e:
+            except FormdefImportRecoverableError:
                 fp.seek(0)
                 formdef = self.formdef_class.import_from_xml(fp, fix_on_error=True)
                 get_session().message = ('info', str(self.import_error_message))
diff --git a/wcs/backoffice/cards.py b/wcs/backoffice/cards.py
index 944b675d..6ef8637c 100644
--- a/wcs/backoffice/cards.py
+++ b/wcs/backoffice/cards.py
@@ -166,7 +166,7 @@ class CardDefPage(FormDefPage):
         )
 
         if (
-            self.formdef.digest_template
+            self.formdef.default_digest_template
             or self.formdef.lateral_template
             or self.formdef.submission_lateral_template
         ):
diff --git a/wcs/backoffice/management.py b/wcs/backoffice/management.py
index 04755c15..3c9591b4 100644
--- a/wcs/backoffice/management.py
+++ b/wcs/backoffice/management.py
@@ -1659,7 +1659,7 @@ class FormPage(Directory):
                 ('any', _('to any users'), 'any'),
             ]
 
-            if isinstance(self.formdef, CardDef) and self.formdef.digest_template:
+            if isinstance(self.formdef, CardDef) and self.formdef.default_digest_template:
                 options.append(('datasource', _('as data source'), 'datasource'))
 
             form.add(
diff --git a/wcs/carddef.py b/wcs/carddef.py
index ab1d2eed..3b4e29ba 100644
--- a/wcs/carddef.py
+++ b/wcs/carddef.py
@@ -158,7 +158,7 @@ class CardDef(FormDef):
     def get_carddefs_as_data_source(cls):
         carddefs_by_id = {}
         for carddef in cls.select(lightweight=True, ignore_errors=True, order_by='name'):
-            if not carddef.digest_template:
+            if not carddef.default_digest_template:
                 continue
             data_source_id = 'carddef:%s' % carddef.url_name
             carddefs_by_id[carddef.id] = carddef
diff --git a/wcs/formdata.py b/wcs/formdata.py
index a738cd09..acc32916 100644
--- a/wcs/formdata.py
+++ b/wcs/formdata.py
@@ -423,7 +423,7 @@ class FormData(StorableObject):
         self.evolution = [evo]
 
     def set_auto_fields(self, *args, **kwargs):
-        fields = {'digest': self.formdef.digest_template}
+        fields = {'digest': self.formdef.default_digest_template}
         if not self.id_display:
             # only set id_display once as it may have been set automatically
             # by interpreting a webservice response.
diff --git a/wcs/formdef.py b/wcs/formdef.py
index 1003ab9a..e0233caf 100644
--- a/wcs/formdef.py
+++ b/wcs/formdef.py
@@ -115,7 +115,7 @@ class FormDef(StorableObject):
     skip_from_360_view = False
     include_download_all_button = False
     appearance_keywords = None
-    digest_template = None
+    digest_templates = None
     lateral_template = None
     submission_lateral_template = None
     drafts_lifespan = None
@@ -142,7 +142,6 @@ class FormDef(StorableObject):
         'internal_identifier',
         'disabled_redirection',
         'appearance_keywords',
-        'digest_template',
         'lateral_template',
         'submission_lateral_template',
         'drafts_lifespan',
@@ -227,6 +226,14 @@ class FormDef(StorableObject):
             del self.__dict__['receiver_id']
             changed = True
 
+        if 'digest_template' in self.__dict__:
+            # migration from a simple template to templates
+            if not self.digest_templates:
+                self.digest_templates = {}
+            self.digest_templates['default'] = self.__dict__['digest_template']
+            del self.__dict__['digest_template']
+            changed = True
+
         if not self.table_name and get_publisher().has_site_option('postgresql'):
             from . import sql
 
@@ -431,6 +438,10 @@ class FormDef(StorableObject):
 
             sql.do_formdef_tables(self, rebuild_views=True, rebuild_global_views=True)
 
+    @property
+    def default_digest_template(self):
+        return (self.digest_templates or {}).get('default')
+
     def get_category(self):
         if self.category_id:
             try:
@@ -1067,6 +1078,14 @@ class FormDef(StorableObject):
             for auth_context in self.required_authentication_contexts:
                 ET.SubElement(element, 'method').text = force_text(auth_context)
 
+        if self.digest_templates:
+            digest_templates = ET.SubElement(root, 'digest_templates')
+            for key, value in self.digest_templates.items():
+                if not value:
+                    continue
+                sub = ET.SubElement(digest_templates, 'template')
+                sub.attrib['key'] = key
+                sub.text = value
         return root
 
     @classmethod
@@ -1262,6 +1281,14 @@ class FormDef(StorableObject):
             for child in node:
                 formdef.required_authentication_contexts.append(str(child.text))
 
+        if tree.find('digest_templates') is not None:
+            digest_templates_node = tree.find('digest_templates')
+            formdef.digest_templates = {}
+            for child in digest_templates_node:
+                key = child.attrib['key']
+                value = xml_node_text(child)
+                formdef.digest_templates[key] = value
+
         if check_datasources:
             # check if datasources are defined
             unknown_datasources = set()
-- 
2.30.2

