From f93ddf1cf4cdf035ef94cd0d63214707505aa76a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Wed, 28 Dec 2022 15:39:22 +0100
Subject: [PATCH 2/2] backoffice: also allow importing fields from cards
 (#61416)

---
 tests/admin_pages/test_form.py | 89 ++++++++++++++++++++++++++++++
 wcs/admin/blocks.py            |  4 +-
 wcs/admin/fields.py            | 99 ++++++++++++++++++----------------
 3 files changed, 145 insertions(+), 47 deletions(-)

diff --git a/tests/admin_pages/test_form.py b/tests/admin_pages/test_form.py
index 1b1f8af00..ab89c89f7 100644
--- a/tests/admin_pages/test_form.py
+++ b/tests/admin_pages/test_form.py
@@ -3644,3 +3644,92 @@ def test_admin_form_inspect(pub):
         block.get_admin_url() + 'inspect'
     )
     assert resp.pyquery('[data-field-id="14"] .parameter-data_source a').attr['href'] == '#invalid-xxx'
+
+
+def test_form_import_fields(pub):
+    create_superuser(pub)
+    create_role(pub)
+
+    CardDef.wipe()
+    FormDef.wipe()
+
+    formdef1 = FormDef()
+    formdef1.name = 'form title'
+    formdef1.fields = [
+        fields.StringField(id='1', label='field 1', type='string'),
+        fields.StringField(id='2', label='field 2', type='string'),
+    ]
+    formdef1.store()
+
+    formdef2 = FormDef()
+    formdef2.name = 'form2 title'
+    formdef2.fields = [
+        fields.StringField(id='1', label='field A', type='string'),
+    ]
+    formdef2.store()
+
+    app = login(get_app(pub))
+    resp = app.get('/backoffice/forms/%s/fields/' % formdef2.id)
+
+    resp = app.get('/backoffice/forms/%s/fields/new' % formdef2.id, status=302).follow()
+    assert 'Submitted form was not filled properly.' in resp.text
+
+    assert len(resp.forms['import-fields']['form'].options) == 3  # (empty, form1, form2)
+    assert resp.pyquery('#import-fields optgroup').length == 0
+
+    carddef = CardDef()
+    carddef.name = 'card title'
+    carddef.fields = [
+        fields.StringField(id='1', label='field 3', type='string'),
+        fields.StringField(id='2', label='field 4', type='string'),
+    ]
+    carddef.store()
+
+    resp = app.get('/backoffice/forms/%s/fields/' % formdef2.id)
+    assert len(resp.forms['import-fields']['form'].options) == 4  # (empty, form1, form2, card1)
+    assert resp.pyquery('#import-fields optgroup').length == 2
+
+    resp.forms['import-fields']['form'] = 'form:%s' % formdef1.id
+    resp = resp.forms['import-fields'].submit().follow()
+    assert 'Submitted form was not filled properly.' not in resp.text
+
+    formdef2.refresh_from_storage()
+    assert [(x.id, x.label) for x in formdef2.fields] == [
+        ('1', 'field A'),
+        ('2', 'field 1'),
+        ('3', 'field 2'),
+    ]
+
+    # import a card
+    resp.forms['import-fields']['form'] = 'card:%s' % carddef.id
+    resp = resp.forms['import-fields'].submit().follow()
+    formdef2.refresh_from_storage()
+    assert [(x.id, x.label) for x in formdef2.fields] == [
+        ('1', 'field A'),
+        ('2', 'field 1'),
+        ('3', 'field 2'),
+        ('4', 'field 3'),
+        ('5', 'field 4'),
+    ]
+
+    # import while on a specific page
+    formdef3 = FormDef()
+    formdef3.name = 'form3 title'
+    formdef3.fields = [
+        fields.PageField(id='1', label='Page 1', type='page'),
+        fields.StringField(id='2', label='field 1', type='string'),
+        fields.PageField(id='3', label='Page 2', type='page'),
+    ]
+    formdef3.store()
+
+    resp = app.get('/backoffice/forms/%s/fields/pages/1/' % formdef3.id)
+    resp.forms['import-fields']['form'] = 'card:%s' % carddef.id
+    resp = resp.forms['import-fields'].submit().follow()
+    formdef3.refresh_from_storage()
+    assert [(x.id, x.label) for x in formdef3.fields] == [
+        ('1', 'Page 1'),
+        ('2', 'field 1'),
+        ('4', 'field 3'),
+        ('5', 'field 4'),
+        ('3', 'Page 2'),
+    ]
diff --git a/wcs/admin/blocks.py b/wcs/admin/blocks.py
index 9462418b9..3707c8f23 100644
--- a/wcs/admin/blocks.py
+++ b/wcs/admin/blocks.py
@@ -98,7 +98,7 @@ class BlockDirectory(FieldsDirectory):
         r += htmltext('</div>')
         return r.getvalue()
 
-    def get_new_field_form(self, page_id):
+    def get_new_field_form_sidebar(self, page_id):
         r = TemplateIO(html=True)
         r += htmltext('<ul id="sidebar-actions">')
         r += htmltext('<li><a href="delete" rel="popup">%s</a></li>') % _('Delete')
@@ -110,7 +110,7 @@ class BlockDirectory(FieldsDirectory):
         r += htmltext('<li><a href="inspect">%s</a></li>') % _('Inspector')
         r += htmltext('<li><a href="settings" rel="popup">%s</a></li>') % _('Settings')
         r += htmltext('</ul>')
-        r += super().get_new_field_form(page_id=page_id)
+        r += super().get_new_field_form_sidebar(page_id=page_id)
         return r.getvalue()
 
     def delete(self):
diff --git a/wcs/admin/fields.py b/wcs/admin/fields.py
index e3ffb43da..b714e95cd 100644
--- a/wcs/admin/fields.py
+++ b/wcs/admin/fields.py
@@ -23,12 +23,13 @@ from quixote.html import TemplateIO, htmlescape, htmltag, htmltext
 
 from wcs import fields
 from wcs.admin import utils
+from wcs.carddef import CardDef
 from wcs.fields import BlockField, get_field_options
 from wcs.formdef import FormDef
 from wcs.qommon import _, errors, get_cfg, misc
 from wcs.qommon.admin.menu import command_icon
 from wcs.qommon.backoffice.menu import html_top
-from wcs.qommon.form import CheckboxWidget, Form, HtmlWidget, SingleSelectWidget, StringWidget
+from wcs.qommon.form import CheckboxWidget, Form, HtmlWidget, OptGroup, SingleSelectWidget, StringWidget
 from wcs.qommon.substitution import CompatibilityNamesDict
 
 
@@ -480,11 +481,11 @@ class FieldsDirectory(Directory):
                     snapshot=self.objectdef.snapshot_object, url_prefix=url_prefix, url_suffix=url_suffix
                 )
         else:
-            get_response().filter['sidebar'] = str(self.get_new_field_form(self.page_id))
+            get_response().filter['sidebar'] = str(self.get_new_field_form_sidebar(self.page_id))
         r += self.index_bottom()
         return r.getvalue()
 
-    def get_new_field_form(self, page_id):
+    def get_new_field_form_sidebar(self, page_id):
         r = TemplateIO(html=True)
         ignore_hard_limits = get_publisher().has_site_option('ignore-hard-limits', default=False)
 
@@ -499,7 +500,17 @@ class FieldsDirectory(Directory):
         r += htmltext('<div id="new-field">')
         r += htmltext('<h3>%s</h3>') % _('New Field')
         get_request().form = None  # ignore the eventual ?page=x
-        form = Form(enctype='multipart/form-data', action='new')
+        form = self.get_new_field_form(page_id)
+        r += form.render()
+        if self.support_import:
+            form = self.get_import_fields_from(page_id)
+            if form:
+                r += form.render()
+        r += htmltext('</div>')
+        return r.getvalue()
+
+    def get_new_field_form(self, page_id):
+        form = Form(enctype='multipart/form-data', action='new', id='new-field')
         if page_id:
             form.add_hidden('page_id', page_id)
         form.add(StringWidget, 'label', title=_('Label'), required=True, size=50)
@@ -511,26 +522,32 @@ class FieldsDirectory(Directory):
             options=get_field_options(self.blacklisted_types),
         )
         form.add_submit('submit', _('Add'))
-        r += form.render()
-        if self.support_import:
-            form = Form(enctype='multipart/form-data', action='new')
-            if page_id:
-                form.add_hidden('page_id', page_id)
-            form.add(
-                SingleSelectWidget,
-                'form',
-                title=_('Or import fields from:'),
-                required=True,
-                options=[(None, '----', None)]
-                + [
-                    (x.id, x.name, x.id)
-                    for x in FormDef.select(order_by='name', lightweight=True, ignore_errors=True)
-                ],
-            )
-            form.add_submit('submit', _('Submit'))
-            r += form.render()
-        r += htmltext('</div>')
-        return r.getvalue()
+        return form
+
+    def get_import_fields_from(self, page_id):
+        formdefs = FormDef.select(order_by='name', lightweight=True, ignore_errors=True)
+        carddefs = CardDef.select(order_by='name', lightweight=True, ignore_errors=True)
+        if not (formdefs or carddefs):
+            return
+        options = [(None, '\u2500' * 5, None)]
+        if formdefs and carddefs:
+            options.append(OptGroup(_('Forms')))
+        options.extend([(f'form:{x.id}', x.name, f'form:{x.id}') for x in formdefs])
+        if formdefs and carddefs:
+            options.append(OptGroup(_('Card Models')))
+        options.extend([(f'card:{x.id}', x.name, f'card:{x.id}') for x in carddefs])
+        form = Form(enctype='multipart/form-data', action='new', id='import-fields')
+        if page_id:
+            form.add_hidden('page_id', page_id)
+        form.add(
+            SingleSelectWidget,
+            'form',
+            title=_('Or import fields from:'),
+            required=True,
+            options=options,
+        )
+        form.add_submit('submit', _('Submit'))
+        return form
 
     def index_top(self):
         r = TemplateIO(html=True)
@@ -640,31 +657,20 @@ class FieldsDirectory(Directory):
         return redirect('.')
 
     def new(self):
-        form = Form(enctype='multipart/form-data', action='new')
-        form.add(StringWidget, 'page_id')
-        form.add(StringWidget, 'label', title=_('Label'), size=50)
-        form.add(
-            SingleSelectWidget, 'type', title=_('Type'), options=get_field_options(self.blacklisted_types)
-        )
-        if FormDef.count():
-            form.add(
-                SingleSelectWidget,
-                'form',
-                title=_('Or import fields from:'),
-                options=[(x.id, x.name, x.id) for x in FormDef.select(order_by='name', ignore_errors=True)],
-            )
+        form = Form(enctype='multipart/form-data')
+        form.add_hidden('page_id')
+        form.add_hidden('label')
+        form.add_hidden('type')
+        form.add_hidden('form')
 
-        if not form.is_submitted() or form.has_errors():
+        if not form.is_submitted():
             get_session().message = ('error', _('Submitted form was not filled properly.'))
             return redirect('.')
 
-        try:
-            page_id = form.get_widget('page_id').parse()
-        except (TypeError, ValueError):
-            page_id = None
+        page_id = form.get_widget('page_id').parse()
 
         redirect_url = '.'
-        if page_id is not None:
+        if page_id:
             redirect_url = './?page=%s' % page_id
             on_page = False
             for i, field in enumerate(self.objectdef.fields):
@@ -690,13 +696,16 @@ class FieldsDirectory(Directory):
             self.objectdef.fields.insert(insertion_point, field)
             self.objectdef.store(comment=self.new_field_history_message % field.ellipsized_label)
         elif form.get_widget('form') and form.get_widget('form').parse():
-            formdef = FormDef.get(form.get_widget('form').parse())
+            form_class, form_id = form.get_widget('form').parse().split(':')
+            if form_class == 'form':
+                formdef = FormDef.get(form_id)
+            else:
+                formdef = CardDef.get(form_id)
             for j, field in enumerate(formdef.fields):
                 field.id = self.objectdef.get_new_field_id()
                 self.objectdef.fields.insert(insertion_point + j, field)
             self.objectdef.store(comment=_('Import of fields from "%s"') % formdef.name)
         else:
             get_session().message = ('error', _('Submitted form was not filled properly.'))
-            return redirect(redirect_url)
 
         return redirect(redirect_url)
-- 
2.39.0

