From f3cd4935ffa81385cfa77779e0f2fc3cfd231b43 Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Tue, 8 Feb 2022 17:32:05 +0100 Subject: [PATCH 01/14] black --- collective/easyform/migration/actions.py | 101 ++++++------ collective/easyform/migration/data.py | 31 +++- collective/easyform/migration/fields.py | 192 ++++++++++++----------- collective/easyform/migration/pfg.py | 73 ++++----- 4 files changed, 209 insertions(+), 188 deletions(-) diff --git a/collective/easyform/migration/actions.py b/collective/easyform/migration/actions.py index c251ff1c..8f10b94b 100644 --- a/collective/easyform/migration/actions.py +++ b/collective/easyform/migration/actions.py @@ -11,19 +11,20 @@ import logging try: - from collective.easyform.interfaces.mailer import default_mail_body + from collective.easyform.interfaces.mailer import default_mail_body except ImportError: - from collective.easyform.config import MAIL_BODY_DEFAULT - def default_mail_body(): - return MAIL_BODY_DEFAULT + from collective.easyform.config import MAIL_BODY_DEFAULT + def default_mail_body(): + return MAIL_BODY_DEFAULT -logger = logging.getLogger('collective.easyform.migration') + +logger = logging.getLogger("collective.easyform.migration") NAMESPACES = { - 'easyform': 'http://namespaces.plone.org/supermodel/easyform', - 'form': 'http://namespaces.plone.org/supermodel/form', + "easyform": "http://namespaces.plone.org/supermodel/easyform", + "form": "http://namespaces.plone.org/supermodel/form", } @@ -34,48 +35,50 @@ def append_body_pt(field, name, value): node.text = default_mail_body() -Type = namedtuple('Type', ['name', 'handler']) -Property = namedtuple('Property', ['name', 'handler']) +Type = namedtuple("Type", ["name", "handler"]) +Property = namedtuple("Property", ["name", "handler"]) TYPES_MAPPING = { - 'FormMailerAdapter': Type('collective.easyform.actions.Mailer', append_field), - 'FormSaveDataAdapter': Type('collective.easyform.actions.SaveData', append_field), - 'FormCustomScriptAdapter': Type('collective.easyform.actions.CustomScript', append_field), + "FormMailerAdapter": Type("collective.easyform.actions.Mailer", append_field), + "FormSaveDataAdapter": Type("collective.easyform.actions.SaveData", append_field), + "FormCustomScriptAdapter": Type( + "collective.easyform.actions.CustomScript", append_field + ), } PROPERTIES_MAPPING = { - 'additional_headers': Property('additional_headers', append_node), - 'bcc_recipients': Property('bcc_recipients', append_node), - 'bccOverride': Property('bccOverride', append_node), - 'body_footer': Property('body_footer', append_node), - 'body_post': Property('body_post', append_node), - 'body_pre': Property('body_pre', append_node), - 'body_pt': Property('body_pt', append_body_pt), - 'body_type': Property('body_type', append_node), - 'cc_recipients': Property('cc_recipients', append_node), - 'ccOverride': Property('ccOverride', append_node), - 'description': Property('description', append_node), - 'execCondition': Property('easyform:execCondition', set_attribute), - 'msg_subject': Property('msg_subject', append_node), - 'recipient_email': Property('recipient_email', append_node), - 'recipient_name': Property('recipient_name', append_node), - 'recipientOverride': Property('recipientOverride', append_node), - 'replyto_field': Property('replyto_field', append_node), - 'senderOverride': Property('senderOverride', append_node), - 'showAll': Property('showAll', append_node), - 'showFields': Property('showFields', append_list_node), - 'subject_field': Property('subject_field', append_node), - 'subjectOverride': Property('subjectOverride', append_node), - 'title': Property('title', append_node), - 'to_field': Property('to_field', append_node), - 'xinfo_headers': Property('xinfo_headers', append_node), - 'ExtraData': Property('ExtraData', append_list_node), - 'DownloadFormat': Property('DownloadFormat', append_node), - 'UseColumnNames': Property('UseColumnNames', append_node), - 'SavedFormInput': Property('SavedFormInput', append_node), - 'ProxyRole': Property('ProxyRole', append_node), - 'ScriptBody': Property('ScriptBody', append_node), + "additional_headers": Property("additional_headers", append_node), + "bcc_recipients": Property("bcc_recipients", append_node), + "bccOverride": Property("bccOverride", append_node), + "body_footer": Property("body_footer", append_node), + "body_post": Property("body_post", append_node), + "body_pre": Property("body_pre", append_node), + "body_pt": Property("body_pt", append_body_pt), + "body_type": Property("body_type", append_node), + "cc_recipients": Property("cc_recipients", append_node), + "ccOverride": Property("ccOverride", append_node), + "description": Property("description", append_node), + "execCondition": Property("easyform:execCondition", set_attribute), + "msg_subject": Property("msg_subject", append_node), + "recipient_email": Property("recipient_email", append_node), + "recipient_name": Property("recipient_name", append_node), + "recipientOverride": Property("recipientOverride", append_node), + "replyto_field": Property("replyto_field", append_node), + "senderOverride": Property("senderOverride", append_node), + "showAll": Property("showAll", append_node), + "showFields": Property("showFields", append_list_node), + "subject_field": Property("subject_field", append_node), + "subjectOverride": Property("subjectOverride", append_node), + "title": Property("title", append_node), + "to_field": Property("to_field", append_node), + "xinfo_headers": Property("xinfo_headers", append_node), + "ExtraData": Property("ExtraData", append_list_node), + "DownloadFormat": Property("DownloadFormat", append_node), + "UseColumnNames": Property("UseColumnNames", append_node), + "SavedFormInput": Property("SavedFormInput", append_node), + "ProxyRole": Property("ProxyRole", append_node), + "ScriptBody": Property("ScriptBody", append_node), } @@ -86,7 +89,7 @@ def pfg_actions(context): continue id_ = obj.getId() props = {} - props['_portal_type'] = obj.portal_type + props["_portal_type"] = obj.portal_type for field in obj.Schema().fields(): name = field.getName() accessor = field.getEditAccessor(obj) @@ -99,18 +102,20 @@ def actions_model(ploneformgen): """Create an actions xml model from a PloneFormGen instance.""" parser = etree.XMLParser(remove_blank_text=True) model = etree.fromstring(ACTIONS_MODEL, parser) - schema = model.find( - '{http://namespaces.plone.org/supermodel/schema}schema') + schema = model.find("{http://namespaces.plone.org/supermodel/schema}schema") for actionname, properties in pfg_actions(ploneformgen): - type_ = TYPES_MAPPING.get(properties['_portal_type']) + type_ = TYPES_MAPPING.get(properties["_portal_type"]) if type_ is None: # ploneformgen has been renamed with _MIGRATION_ suffix # hence, we remove 11 chars pfgpath = "/".join(ploneformgen.getPhysicalPath())[:-11] logger.warning( "Ignoring field '%s' of type '%s' of form '%s.", - actionname, properties['_portal_type'], pfgpath) + actionname, + properties["_portal_type"], + pfgpath, + ) continue field = type_.handler(schema, type_.name, actionname, properties) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index 3ee7bfa0..7997a79b 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -13,11 +13,11 @@ import logging -logger = logging.getLogger('collective.easyform.migration') +logger = logging.getLogger("collective.easyform.migration") def migrate_saved_data(ploneformgen, easyform): - for data_adapter in ploneformgen.objectValues('FormSaveDataAdapter'): + for data_adapter in ploneformgen.objectValues("FormSaveDataAdapter"): actions = get_actions(easyform) action = actions.get(data_adapter.getId()) schema = get_fields(easyform) @@ -26,16 +26,18 @@ def migrate_saved_data(ploneformgen, easyform): for idx, row in enumerate(data_adapter.getSavedFormInput()): if len(row) != len(cols): logger.warning( - 'Number of columns does not match. Skipping row %s in ' - 'data adapter %s/%s', idx, - '/'.join(easyform.getPhysicalPath()), - data_adapter.getId()) + "Number of columns does not match. Skipping row %s in " + "data adapter %s/%s", + idx, + "/".join(easyform.getPhysicalPath()), + data_adapter.getId(), + ) continue data = {} for key, value in zip(cols, row): field = schema.get(key) - value = value.decode('utf8') - if IFromUnicode.providedBy(field): + value = value.decode("utf8") + if IFromUnicode.providedBy(field) and value: value = field.fromUnicode(value) elif IDatetime.providedBy(field) and value: value = DateTime(value).asdatetime() @@ -46,6 +48,19 @@ def migrate_saved_data(ploneformgen, easyform): value = set(literal_eval(value)) except ValueError: pass + except: + logger.exception( + "There was an error in the following with the following data" + "data adapter %s/%s", + "/".join(easyform.getPhysicalPath()), + data_adapter.getId(), + ) + print( + "There was an error in the following with the following data" + "data adapter %s/%s", + "/".join(easyform.getPhysicalPath()), + data_adapter.getId(), + ) elif INamedBlobFileField.providedBy(field): value = None data[key] = value diff --git a/collective/easyform/migration/fields.py b/collective/easyform/migration/fields.py index f6208537..8fbd48f3 100644 --- a/collective/easyform/migration/fields.py +++ b/collective/easyform/migration/fields.py @@ -10,58 +10,58 @@ import six -logger = logging.getLogger('collective.easyform.migration') +logger = logging.getLogger("collective.easyform.migration") NAMESPACES = { - 'easyform': 'http://namespaces.plone.org/supermodel/easyform', - 'form': 'http://namespaces.plone.org/supermodel/form', + "easyform": "http://namespaces.plone.org/supermodel/easyform", + "form": "http://namespaces.plone.org/supermodel/form", } def append_field(schema, type_, name, properties): - field = etree.SubElement(schema, u'field') - field.set(u'name', name) - field.set(u'type', type_) + field = etree.SubElement(schema, u"field") + field.set(u"name", name) + field.set(u"type", type_) return field def append_label_field(schema, type_, name, properties): field = append_field(schema, type_, name, properties) - append_node(field, u'required', u'False') + append_node(field, u"required", u"False") return field def append_date_field(schema, type_, name, properties): - if properties.get('fgShowHM', False): - return append_field(schema, u'zope.schema.Datetime', name, properties) + if properties.get("fgShowHM", False): + return append_field(schema, u"zope.schema.Datetime", name, properties) else: - return append_field(schema, u'zope.schema.Date', name, properties) + return append_field(schema, u"zope.schema.Date", name, properties) def append_fieldset(schema, type_, name, properties): - fieldset = etree.SubElement(schema, u'fieldset') - fieldset.set(u'name', name) + fieldset = etree.SubElement(schema, u"fieldset") + fieldset.set(u"name", name) return fieldset def set_attribute(field, name, value): - if u':' in name: - ns, attr = name.split(':') + if u":" in name: + ns, attr = name.split(":") ns = NAMESPACES.get(ns, ns) - field.set(u'{{{}}}{}'.format(ns, attr), value) + field.set(u"{{{}}}{}".format(ns, attr), value) else: field.set(name, value) def append_node(field, name, value): - if ':' in name: - ns, name = name.split(':') + if ":" in name: + ns, name = name.split(":") ns = NAMESPACES.get(ns, ns) - name = '{{{}}}{}'.format(ns, name) + name = "{{{}}}{}".format(ns, name) node = etree.SubElement(field, name) if isinstance(value, (list, tuple)): - value = u' '.join(value) + value = u" ".join(value) node.text = value return node @@ -69,77 +69,78 @@ def append_node(field, name, value): def append_list_node(field, name, value): node = append_node(field, name, None) for val in value: - el = etree.SubElement(node, 'element') - if '|' in val: - key, val = val.split('|') - el.set('key', key) + el = etree.SubElement(node, "element") + if "|" in val: + key, val = val.split("|") + el.set("key", key) el.text = val def append_required_node(field, name, value): - if value == u'False': + if value == u"False": append_node(field, name, value) def append_maxlength_node(field, name, value): - if value != u'0': + if value != u"0": append_node(field, name, value) def append_vocab_node(field, name, value): - if field.get('type') == 'zope.schema.Set': - node = etree.SubElement(field, 'value_type') - node.set('type', 'zope.schema.Choice') + if field.get("type") == "zope.schema.Set": + node = etree.SubElement(field, "value_type") + node.set("type", "zope.schema.Choice") else: node = field - append_list_node(node, u'values', value) + append_list_node(node, u"values", value) def append_default_node(field, name, value): if isinstance(value, list): return - if field.get('type') == 'collective.easyform.fields.RichLabel': - append_node(field, 'rich_label', value) + if field.get("type") == "collective.easyform.fields.RichLabel": + append_node(field, "rich_label", value) else: append_node(field, name, value) def append_widget_node(field, name, value): node = append_node(field, name, None) - type_ = field.get('type') - if type_ == 'zope.schema.Set': - if value == 'select': - widget = 'z3c.form.browser.select.CollectionSelectFieldWidget' + type_ = field.get("type") + if type_ == "zope.schema.Set": + if value == "select": + widget = "z3c.form.browser.select.CollectionSelectFieldWidget" else: - widget = 'z3c.form.browser.checkbox.CheckBoxFieldWidget' + widget = "z3c.form.browser.checkbox.CheckBoxFieldWidget" else: - if value == 'select': - widget = 'z3c.form.browser.select.ChoiceWidgetDispatcher' + if value == "select": + widget = "z3c.form.browser.select.ChoiceWidgetDispatcher" else: - widget = 'z3c.form.browser.radio.RadioFieldWidget' - set_attribute(node, 'type', widget) + widget = "z3c.form.browser.radio.RadioFieldWidget" + set_attribute(node, "type", widget) def append_or_set_title(field, name, value): - if field.tag == 'fieldset': - set_attribute(field, 'label', value) + if field.tag == "fieldset": + set_attribute(field, "label", value) else: append_node(field, name, value) + def append_or_set_description(field, name, value): - if field.tag == 'fieldset': - set_attribute(field, 'description', value) + if field.tag == "fieldset": + set_attribute(field, "description", value) else: append_node(field, name, value) def convert_tales_expressions(value): - if value == u'here/memberEmail': + if value == u"here/memberEmail": return u"python:member and member.getProperty('email', '') or ''" - elif value == u'here/memberFullName': + elif value == u"here/memberFullName": return u"python:member and member.getProperty('fullname', '') or ''" - elif value == u'here/memberId': + elif value == u"here/memberId": return u"python:member and member.id or ''" return value @@ -149,59 +150,61 @@ def to_text(value): return [to_text(v) for v in value] value = str(value) if six.PY2: - value = value.decode('utf8') + value = value.decode("utf8") return value -Type = namedtuple('Type', ['name', 'handler']) -Property = namedtuple('Property', ['name', 'handler']) +Type = namedtuple("Type", ["name", "handler"]) +Property = namedtuple("Property", ["name", "handler"]) TYPES_MAPPING = { - 'FormStringField': Type('zope.schema.TextLine', append_field), - 'FormPasswordField': Type('zope.schema.Password', append_field), - 'FormIntegerField': Type('zope.schema.Int', append_field), - 'FormFixedPointField': Type('zope.schema.Float', append_field), - 'FormBooleanField': Type('zope.schema.Bool', append_field), - 'FormDateField': Type('zope.schema.Date', append_date_field), - 'FormLabelField': Type('collective.easyform.fields.Label', append_label_field), - 'FormLinesField': Type('zope.schema.Text', append_field), - 'FormSelectionField': Type('zope.schema.Choice', append_field), - 'FormMultiSelectionField': Type('zope.schema.Set', append_field), - 'FormTextField': Type('zope.schema.Text', append_field), - 'FormRichTextField': Type('plone.app.textfield.RichText', append_field), - 'FormRichLabelField': Type('collective.easyform.fields.RichLabel', append_label_field), - 'FormFileField': Type('plone.namedfile.field.NamedBlobFile', append_field), - 'FormCaptchaField': Type('collective.easyform.fields.ReCaptcha', append_field), - 'FieldsetStart': Type('', append_fieldset), - 'FieldsetEnd': Type('', None), + "FormStringField": Type("zope.schema.TextLine", append_field), + "FormPasswordField": Type("zope.schema.Password", append_field), + "FormIntegerField": Type("zope.schema.Int", append_field), + "FormFixedPointField": Type("zope.schema.Float", append_field), + "FormBooleanField": Type("zope.schema.Bool", append_field), + "FormDateField": Type("zope.schema.Date", append_date_field), + "FormLabelField": Type("collective.easyform.fields.Label", append_label_field), + "FormLinesField": Type("zope.schema.Text", append_field), + "FormSelectionField": Type("zope.schema.Choice", append_field), + "FormMultiSelectionField": Type("zope.schema.Set", append_field), + "FormTextField": Type("zope.schema.Text", append_field), + "FormRichTextField": Type("plone.app.textfield.RichText", append_field), + "FormRichLabelField": Type( + "collective.easyform.fields.RichLabel", append_label_field + ), + "FormFileField": Type("plone.namedfile.field.NamedBlobFile", append_field), + "FormCaptchaField": Type("collective.easyform.fields.ReCaptcha", append_field), + "FieldsetStart": Type("", append_fieldset), + "FieldsetEnd": Type("", None), } PROPERTIES_MAPPING = { - 'description': Property('description', append_or_set_description), - 'fgDefault': Property('default', append_default_node), - 'fgmaxlength': Property('max_length', append_maxlength_node), - 'fgsize': None, # Not available in collective.easyform - 'fgStringValidator': Property('easyform:validators', set_attribute), - 'fgTDefault': Property('easyform:TDefault', set_attribute), - 'fgTEnabled': Property('easyform:TEnabled', set_attribute), - 'fgTValidator': Property('easyform:TValidator', set_attribute), - 'fgTVocabulary': None, # Not available in collective.easyform - 'fgVocabulary': Property('values', append_vocab_node), - 'hidden': Property('easyform:THidden', set_attribute), - 'maxval': Property('max', append_node), - 'minval': Property('min', append_node), - 'placeholder': None, # Not available in collective.easyform - 'required': Property('required', append_required_node), - 'serverSide': Property('easyform:serverSide', set_attribute), - 'title': Property('title', append_or_set_title), - 'fgFormat': Property('form:widget', append_widget_node), + "description": Property("description", append_or_set_description), + "fgDefault": Property("default", append_default_node), + "fgmaxlength": Property("max_length", append_maxlength_node), + "fgsize": None, # Not available in collective.easyform + "fgStringValidator": Property("easyform:validators", set_attribute), + "fgTDefault": Property("easyform:TDefault", set_attribute), + "fgTEnabled": Property("easyform:TEnabled", set_attribute), + "fgTValidator": Property("easyform:TValidator", set_attribute), + "fgTVocabulary": None, # Not available in collective.easyform + "fgVocabulary": Property("values", append_vocab_node), + "hidden": Property("easyform:THidden", set_attribute), + "maxval": Property("max", append_node), + "minval": Property("min", append_node), + "placeholder": None, # Not available in collective.easyform + "required": Property("required", append_required_node), + "serverSide": Property("easyform:serverSide", set_attribute), + "title": Property("title", append_or_set_title), + "fgFormat": Property("form:widget", append_widget_node), } def pfg_fields_properties(obj): id_ = obj.getId() props = {} - props['_portal_type'] = obj.portal_type + props["_portal_type"] = obj.portal_type for field in obj.Schema().fields(): name = field.getName() accessor = field.getEditAccessor(obj) @@ -237,11 +240,10 @@ def fields_model(ploneformgen): """Create a fields xml model from a PloneFormGen instance.""" parser = etree.XMLParser(remove_blank_text=True) model = etree.fromstring(FIELDS_MODEL, parser) - schema = model.find( - '{http://namespaces.plone.org/supermodel/schema}schema') + schema = model.find("{http://namespaces.plone.org/supermodel/schema}schema") for fieldname, properties in pfg_fields(ploneformgen): - portal_type = properties['_portal_type'] - if portal_type == 'FieldsetEnd': + portal_type = properties["_portal_type"] + if portal_type == "FieldsetEnd": schema = schema.getparent() continue @@ -251,7 +253,11 @@ def fields_model(ploneformgen): # hence, we remove 11 chars pfgpath = "/".join(ploneformgen.getPhysicalPath())[:-11] logger.warning( - "Ignoring field '%s' of type '%s' of form '%s'.", fieldname, portal_type, pfgpath) + "Ignoring field '%s' of type '%s' of form '%s'.", + fieldname, + portal_type, + pfgpath, + ) continue if type_.handler is None: @@ -266,12 +272,12 @@ def fields_model(ploneformgen): value = to_text(value) # Convert TALES expressions with PFG specific methods - if name.startswith('fgT'): + if name.startswith("fgT"): value = convert_tales_expressions(value) prop.handler(field, prop.name, value) - if portal_type == 'FieldsetStart': + if portal_type == "FieldsetStart": schema = field return etree.tostring(model, pretty_print=True) diff --git a/collective/easyform/migration/pfg.py b/collective/easyform/migration/pfg.py index b38f674d..74b978b9 100644 --- a/collective/easyform/migration/pfg.py +++ b/collective/easyform/migration/pfg.py @@ -24,32 +24,34 @@ import transaction -logger = logging.getLogger('collective.easyform.migration') +logger = logging.getLogger("collective.easyform.migration") -Field = namedtuple('Type', ['name', 'handler']) +Field = namedtuple("Type", ["name", "handler"]) FIELD_MAPPING = { - 'submitLabel': Field('submitLabel', migrate_simplefield), - 'resetLabel': Field('resetLabel', migrate_simplefield), - 'useCancelButton': Field('useCancelButton', migrate_simplefield), - 'forceSSL': Field('forceSSL', migrate_simplefield), - 'formPrologue': Field('formPrologue', migrate_richtextfield), - 'formEpilogue': Field('formEpilogue', migrate_richtextfield), - 'thanksPageOverride': Field('thanksPageOverride', migrate_simplefield), - 'formActionOverride': Field('formActionOverride', migrate_simplefield), - 'onDisplayOverride': Field('onDisplayOverride', migrate_simplefield), - 'afterValidationOverride': Field('afterValidationOverride', migrate_simplefield), # noqa - 'headerInjection': Field('headerInjection', migrate_simplefield), - 'checkAuthenticator': Field('CSRFProtection', migrate_simplefield), + "submitLabel": Field("submitLabel", migrate_simplefield), + "resetLabel": Field("resetLabel", migrate_simplefield), + "useCancelButton": Field("useCancelButton", migrate_simplefield), + "forceSSL": Field("forceSSL", migrate_simplefield), + "formPrologue": Field("formPrologue", migrate_richtextfield), + "formEpilogue": Field("formEpilogue", migrate_richtextfield), + "thanksPageOverride": Field("thanksPageOverride", migrate_simplefield), + "formActionOverride": Field("formActionOverride", migrate_simplefield), + "onDisplayOverride": Field("onDisplayOverride", migrate_simplefield), + "afterValidationOverride": Field( + "afterValidationOverride", migrate_simplefield + ), # noqa + "headerInjection": Field("headerInjection", migrate_simplefield), + "checkAuthenticator": Field("CSRFProtection", migrate_simplefield), } class PloneFormGenMigrator(ATCTContentMigrator): """Migrator for PFG to easyform""" - src_portal_type = 'FormFolder' - src_meta_type = 'FormFolder' - dst_portal_type = 'EasyForm' + src_portal_type = "FormFolder" + src_meta_type = "FormFolder" + dst_portal_type = "EasyForm" dst_meta_type = None # not used def migrate_ploneformgen(self): @@ -62,25 +64,23 @@ def migrate_ploneformgen(self): def migrate(self, unittest=0): super(PloneFormGenMigrator, self).migrate() - logger.info( - 'Migrated FormFolder %s', - '/'.join(self.new.getPhysicalPath())) + logger.info("Migrated FormFolder %s", "/".join(self.new.getPhysicalPath())) class IMigratePloneFormGenFormSchema(model.Schema): dry_run = schema.Bool( - title=u'Dry run', + title=u"Dry run", required=True, default=False, ) class MigratePloneFormGenForm(AutoExtensibleForm, Form): - label = u'Migrate PloneFormGen Forms' + label = u"Migrate PloneFormGen Forms" ignoreContext = True schema = IMigratePloneFormGenFormSchema - @buttonAndHandler(u'Migrate') + @buttonAndHandler(u"Migrate") def handle_migrate(self, action): data, errors = self.extractData() if len(errors) > 0: @@ -89,18 +89,17 @@ def handle_migrate(self, action): self.log = StringIO() handler = logging.StreamHandler(self.log) logger.addHandler(handler) - formatter = logging.Formatter( - '%(asctime)s %(levelname)s %(name)s %(message)s') + formatter = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s") handler.setFormatter(formatter) self.migrate() self.migration_done = True - if data.get('dry_run', False): + if data.get("dry_run", False): transaction.abort() - logger.info(u'PloneFormGen migration finished (dry run)') + logger.info(u"PloneFormGen migration finished (dry run)") else: - logger.info(u'PloneFormGen migration finished') + logger.info(u"PloneFormGen migration finished") def migrate(self): alsoProvides(self.request, IDisableCSRFProtection) @@ -108,26 +107,22 @@ def migrate(self): # Switch linkintegrity off temporarily. ptool = getToolByName(self.context, "portal_properties") - site_props = getattr(ptool, 'site_properties', None) + site_props = getattr(ptool, "site_properties", None) link_integrity = False - if site_props and site_props.hasProperty( - 'enable_link_integrity_checks'): + if site_props and site_props.hasProperty("enable_link_integrity_checks"): link_integrity = site_props.getProperty( - 'enable_link_integrity_checks', False) + "enable_link_integrity_checks", False + ) if link_integrity: - site_props.manage_changeProperties( - enable_link_integrity_checks=False) + site_props.manage_changeProperties(enable_link_integrity_checks=False) migrate(portal, PloneFormGenMigrator) # Switch linkintegrity back on, if needed if link_integrity: - site_props.manage_changeProperties( - enable_link_integrity_checks=True - ) - + site_props.manage_changeProperties(enable_link_integrity_checks=True) def render(self): - if getattr(self, 'migration_done', False): + if getattr(self, "migration_done", False): return self.log.getvalue() return super(MigratePloneFormGenForm, self).render() From ca77dee6531629ca11e4ea41f39bc09bc7b7e59e Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Wed, 9 Feb 2022 17:19:17 +0100 Subject: [PATCH 02/14] Add migration for thanksPage values Context: thankspage was not migrated, even when available --- collective/easyform/migration/pfg.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/collective/easyform/migration/pfg.py b/collective/easyform/migration/pfg.py index 74b978b9..0f926764 100644 --- a/collective/easyform/migration/pfg.py +++ b/collective/easyform/migration/pfg.py @@ -60,12 +60,31 @@ def migrate_ploneformgen(self): self.new.fields_model = fields_model(self.old) self.new.actions_model = actions_model(self.old) + self.migrate_thankyou_page() + migrate_saved_data(self.old, self.new) def migrate(self, unittest=0): super(PloneFormGenMigrator, self).migrate() logger.info("Migrated FormFolder %s", "/".join(self.new.getPhysicalPath())) + def migrate_thankyou_page(self): + pfg_thankspage = self.old.get(self.old.getThanksPage()) + if pfg_thankspage: + ef = self.new + + ef.thankstitle = pfg_thankspage.title + ef.thanksdescription = pfg_thankspage.Description() + ef.showAll = pfg_thankspage.showAll + ef.showFields = pfg_thankspage.showFields + ef.includeEmpties = pfg_thankspage.includeEmpties + Field("thanksPrologue", migrate_richtextfield).handler( + pfg_thankspage, ef, "thanksPrologue", "thanksPrologue" + ) + Field("thanksEpilogue", migrate_richtextfield).handler( + pfg_thankspage, ef, "thanksEpilogue", "thanksEpilogue" + ) + class IMigratePloneFormGenFormSchema(model.Schema): dry_run = schema.Bool( @@ -115,7 +134,6 @@ def migrate(self): ) if link_integrity: site_props.manage_changeProperties(enable_link_integrity_checks=False) - migrate(portal, PloneFormGenMigrator) # Switch linkintegrity back on, if needed From 79644b267899bca84fb36ae00763415edb8999aa Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Thu, 10 Feb 2022 14:07:17 +0100 Subject: [PATCH 03/14] bugfix: xinfo_headers is a list --- collective/easyform/migration/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collective/easyform/migration/actions.py b/collective/easyform/migration/actions.py index 8f10b94b..27ac5aee 100644 --- a/collective/easyform/migration/actions.py +++ b/collective/easyform/migration/actions.py @@ -72,7 +72,7 @@ def append_body_pt(field, name, value): "subjectOverride": Property("subjectOverride", append_node), "title": Property("title", append_node), "to_field": Property("to_field", append_node), - "xinfo_headers": Property("xinfo_headers", append_node), + "xinfo_headers": Property("xinfo_headers", append_list_node), "ExtraData": Property("ExtraData", append_list_node), "DownloadFormat": Property("DownloadFormat", append_node), "UseColumnNames": Property("UseColumnNames", append_node), From 281dd2b65745597d4eb3825ac33abd630b1239af Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Mon, 21 Feb 2022 10:27:07 +0100 Subject: [PATCH 04/14] Improve logging to catch broken data --- collective/easyform/migration/data.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index 7997a79b..a0a90223 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -46,20 +46,14 @@ def migrate_saved_data(ploneformgen, easyform): elif ISet.providedBy(field): try: value = set(literal_eval(value)) - except ValueError: - pass - except: + except (ValueError, SyntaxError, TypeError): logger.exception( - "There was an error in the following with the following data" - "data adapter %s/%s", - "/".join(easyform.getPhysicalPath()), - data_adapter.getId(), - ) - print( - "There was an error in the following with the following data" - "data adapter %s/%s", - "/".join(easyform.getPhysicalPath()), - data_adapter.getId(), + ":There was a an error for {}:'{}' in the following data adapter {}/{}. The value was skipped during migration".format( + key, + value, + "/".join(easyform.getPhysicalPath()), + data_adapter.getId(), + ) ) elif INamedBlobFileField.providedBy(field): value = None From 1617427cbfd1b5444efc11ba9d0eb389adeec24f Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Wed, 9 Mar 2022 12:17:08 +0100 Subject: [PATCH 05/14] Improve to_text function --- collective/easyform/migration/fields.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/collective/easyform/migration/fields.py b/collective/easyform/migration/fields.py index 8fbd48f3..5d404bef 100644 --- a/collective/easyform/migration/fields.py +++ b/collective/easyform/migration/fields.py @@ -5,6 +5,7 @@ from Products.PloneFormGen.content.fields import FGFieldsetStart from Products.PloneFormGen.content.fieldsBase import BaseFormField from Products.PloneFormGen.interfaces import IPloneFormGenFieldset +from Products.CMFPlone.utils import safe_unicode import logging import six @@ -148,9 +149,10 @@ def convert_tales_expressions(value): def to_text(value): if isinstance(value, (list, tuple)): return [to_text(v) for v in value] - value = str(value) - if six.PY2: - value = value.decode("utf8") + elif isinstance(value, (six.text_type, six.binary_type)): + value = safe_unicode(value) + else: + value = str(value) return value From f9ea0a41d9cc41682d4762e1a26d78c95b0163f6 Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Wed, 9 Mar 2022 12:20:35 +0100 Subject: [PATCH 06/14] Improve Error handling in form migration Context: Broken Data stopped migration. Needed better error handling en logging for data. Also removed SavedFormInput from actions model, as data migration happened elsewhere --- collective/easyform/migration/actions.py | 13 +++++- collective/easyform/migration/data.py | 54 +++++++++++++++--------- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/collective/easyform/migration/actions.py b/collective/easyform/migration/actions.py index 27ac5aee..7fa1064e 100644 --- a/collective/easyform/migration/actions.py +++ b/collective/easyform/migration/actions.py @@ -76,7 +76,6 @@ def append_body_pt(field, name, value): "ExtraData": Property("ExtraData", append_list_node), "DownloadFormat": Property("DownloadFormat", append_node), "UseColumnNames": Property("UseColumnNames", append_node), - "SavedFormInput": Property("SavedFormInput", append_node), "ProxyRole": Property("ProxyRole", append_node), "ScriptBody": Property("ScriptBody", append_node), } @@ -127,7 +126,17 @@ def actions_model(ploneformgen): value = to_text(value) - prop.handler(field, prop.name, value) + try: + prop.handler(field, prop.name, value) + except ValueError: + logger.exception( + "FAIL on '{}' field of PFG form {}/{}.".format( + name, + "/".join(ploneformgen.getPhysicalPath()), + ploneformgen.getId(), + ) + ) + continue return etree.tostring(model, pretty_print=True) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index a0a90223..a93cf281 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -3,12 +3,14 @@ from collective.easyform.api import get_actions from collective.easyform.api import get_fields from collective.easyform.interfaces import ISaveData -from DateTime import DateTime +import DateTime from plone.namedfile.interfaces import INamedBlobFileField from zope.schema.interfaces import IDate from zope.schema.interfaces import IDatetime from zope.schema.interfaces import IFromUnicode from zope.schema.interfaces import ISet +from zope.schema.interfaces import ValidationError + import logging @@ -35,27 +37,37 @@ def migrate_saved_data(ploneformgen, easyform): continue data = {} for key, value in zip(cols, row): - field = schema.get(key) - value = value.decode("utf8") - if IFromUnicode.providedBy(field) and value: - value = field.fromUnicode(value) - elif IDatetime.providedBy(field) and value: - value = DateTime(value).asdatetime() - elif IDate.providedBy(field) and value: - value = DateTime(value).asdatetime().date() - elif ISet.providedBy(field): - try: + try: + field = schema.get(key) + value = value.decode("utf8") + if IFromUnicode.providedBy(field) and value: + value = field.fromUnicode(value) + elif IDatetime.providedBy(field) and value: + value = DateTime.DateTime(value).asdatetime() + elif IDate.providedBy(field) and value: + value = DateTime.DateTime(value).asdatetime().date() + elif ISet.providedBy(field): value = set(literal_eval(value)) - except (ValueError, SyntaxError, TypeError): - logger.exception( - ":There was a an error for {}:'{}' in the following data adapter {}/{}. The value was skipped during migration".format( - key, - value, - "/".join(easyform.getPhysicalPath()), - data_adapter.getId(), - ) + elif INamedBlobFileField.providedBy(field): + value = None + except ( + ValueError, + TypeError, + ValidationError, + SyntaxError, + DateTime.interfaces.SyntaxError, + ): + # Exceptions above are often due to long living Forms, where users have changed their minds about + # the Field formats/widgets... + # Older datarows can break in these cases + logger.exception( + "Error for {}:'{}' in the {}/{} data adapter. Value was skipped during migration".format( + key, + value, + "/".join(easyform.getPhysicalPath()), + data_adapter.getId(), ) - elif INamedBlobFileField.providedBy(field): - value = None + ) + continue data[key] = value action.addDataRow(data) From c2e385f6c391b4070fe570270e11e92154158516 Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Tue, 22 Mar 2022 15:44:25 +0100 Subject: [PATCH 07/14] Warn only of skipping first row when column count mismatch Context: broken forms quickly filled up logs, which made them less usefull --- collective/easyform/migration/data.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index a93cf281..7a977d11 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -25,14 +25,20 @@ def migrate_saved_data(ploneformgen, easyform): schema = get_fields(easyform) if ISaveData.providedBy(action): cols = data_adapter.getColumnNames() + column_count_mismatch = False for idx, row in enumerate(data_adapter.getSavedFormInput()): if len(row) != len(cols): - logger.warning( - "Number of columns does not match. Skipping row %s in " - "data adapter %s/%s", + if not column_count_mismatch: + logger.warning( + "Number of columns does not match for all rows. Some data were skipped in " + "data adapter %s/%s", + "/".join(easyform.getPhysicalPath()), + data_adapter.getId(), + ) + column_count_mismatch = True + logger.info( + "Column count mismatch at row %s", idx, - "/".join(easyform.getPhysicalPath()), - data_adapter.getId(), ) continue data = {} @@ -61,13 +67,20 @@ def migrate_saved_data(ploneformgen, easyform): # the Field formats/widgets... # Older datarows can break in these cases logger.exception( - "Error for {}:'{}' in the {}/{} data adapter. Value was skipped during migration".format( + u"Error for {}:'{}' in the {}/{} data adapter. Value was skipped during migration".format( key, value, "/".join(easyform.getPhysicalPath()), data_adapter.getId(), ) ) - continue + logger.warning( + "To Keep data entigrity, the data was skipped for migration in " + "data adapter %s/%s", + "/".join(easyform.getPhysicalPath()), + data_adapter.getId(), + ) + # continue + return data[key] = value action.addDataRow(data) From c7de78b4d61e4896438d5a9a20ba7f1c785f5d6e Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Wed, 30 Mar 2022 11:17:06 +0200 Subject: [PATCH 08/14] safer logging & clear saveddate in broken forms context: don't migrate any data if proken data entries (to preserve data integrity) safer logging to catch problems with user data --- collective/easyform/migration/data.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index 7a977d11..c3bc3dc1 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -5,6 +5,7 @@ from collective.easyform.interfaces import ISaveData import DateTime from plone.namedfile.interfaces import INamedBlobFileField +from Products.CMFPlone.utils import safe_unicode from zope.schema.interfaces import IDate from zope.schema.interfaces import IDatetime from zope.schema.interfaces import IFromUnicode @@ -66,21 +67,33 @@ def migrate_saved_data(ploneformgen, easyform): # Exceptions above are often due to long living Forms, where users have changed their minds about # the Field formats/widgets... # Older datarows can break in these cases + + try: + value_to_log = safe_unicode(value) + except UnicodeError: + # in case of file field, try to log the file name + # which is seperated with a colon + if ':' in value: + value_to_log = safe_unicode(value.split(':')[0]) + else: + # xxx we hope we are lucky that there are no broken characters in the first 50 + value_to_log = safe_unicode(value)[:50] + logger.exception( - u"Error for {}:'{}' in the {}/{} data adapter. Value was skipped during migration".format( + u"Error for {}:'{}' in the {}/{} data adapter.".format( key, - value, + value_to_log, "/".join(easyform.getPhysicalPath()), data_adapter.getId(), ) ) logger.warning( - "To Keep data entigrity, the data was skipped for migration in " - "data adapter %s/%s", + "BEWARE: to keep data integrity, the data was not migrated for " + "data adapter %s/%s.", "/".join(easyform.getPhysicalPath()), data_adapter.getId(), ) - # continue + action.clearSavedFormInput() return data[key] = value action.addDataRow(data) From 856be3ed6f317c43bde8f7135261b5ee104cae2f Mon Sep 17 00:00:00 2001 From: Thibaut Born Date: Tue, 3 May 2022 13:20:49 +0200 Subject: [PATCH 09/14] Add additional error to except Context: TimeError issues weren't caught and stopped the migration --- collective/easyform/migration/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index c3bc3dc1..51d2f19f 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -12,7 +12,6 @@ from zope.schema.interfaces import ISet from zope.schema.interfaces import ValidationError - import logging @@ -63,6 +62,7 @@ def migrate_saved_data(ploneformgen, easyform): ValidationError, SyntaxError, DateTime.interfaces.SyntaxError, + DateTime.interfaces.TimeError, ): # Exceptions above are often due to long living Forms, where users have changed their minds about # the Field formats/widgets... From 995e5abfba5acd26fb7e33b686a8d2a3f361098b Mon Sep 17 00:00:00 2001 From: Godefroid Chapelle Date: Wed, 20 Jul 2022 16:34:59 +0200 Subject: [PATCH 10/14] backport Likert field from 4.x --- collective/easyform/browser/configure.zcml | 1 + collective/easyform/browser/likert.py | 65 ++++++++++ collective/easyform/browser/likert.zcml | 40 +++++++ collective/easyform/browser/likert_display.pt | 22 ++++ collective/easyform/browser/likert_hidden.pt | 13 ++ collective/easyform/browser/likert_input.pt | 29 +++++ collective/easyform/fields.py | 40 +++++++ collective/easyform/fields.zcml | 8 ++ collective/easyform/interfaces.py | 27 +++++ collective/easyform/migration/fields.py | 3 + collective/easyform/tests/testLikert.py | 112 ++++++++++++++++++ 11 files changed, 360 insertions(+) create mode 100644 collective/easyform/browser/likert.py create mode 100644 collective/easyform/browser/likert.zcml create mode 100644 collective/easyform/browser/likert_display.pt create mode 100644 collective/easyform/browser/likert_hidden.pt create mode 100644 collective/easyform/browser/likert_input.pt create mode 100644 collective/easyform/tests/testLikert.py diff --git a/collective/easyform/browser/configure.zcml b/collective/easyform/browser/configure.zcml index 9b76b820..136a16b7 100644 --- a/collective/easyform/browser/configure.zcml +++ b/collective/easyform/browser/configure.zcml @@ -6,6 +6,7 @@ + + + + + + + + + + + + + + + diff --git a/collective/easyform/browser/likert_display.pt b/collective/easyform/browser/likert_display.pt new file mode 100644 index 00000000..6eb3ae32 --- /dev/null +++ b/collective/easyform/browser/likert_display.pt @@ -0,0 +1,22 @@ + + + diff --git a/collective/easyform/browser/likert_hidden.pt b/collective/easyform/browser/likert_hidden.pt new file mode 100644 index 00000000..a508ca7d --- /dev/null +++ b/collective/easyform/browser/likert_hidden.pt @@ -0,0 +1,13 @@ +
+ +
diff --git a/collective/easyform/browser/likert_input.pt b/collective/easyform/browser/likert_input.pt new file mode 100644 index 00000000..f2b469f8 --- /dev/null +++ b/collective/easyform/browser/likert_input.pt @@ -0,0 +1,29 @@ + + + + + + + + + + +
  +
+Question Number One + + + +
+ diff --git a/collective/easyform/fields.py b/collective/easyform/fields.py index 19fbaee5..11db2479 100644 --- a/collective/easyform/fields.py +++ b/collective/easyform/fields.py @@ -7,6 +7,7 @@ from collective.easyform.interfaces import IFieldExtender from collective.easyform.interfaces import ILabel from collective.easyform.interfaces import IReCaptcha +from collective.easyform.interfaces import ILikert from collective.easyform.interfaces import IRichLabel from collective.easyform.validators import IFieldValidator from plone.schemaeditor.fields import FieldFactory @@ -140,3 +141,42 @@ class ReCaptcha(TextLine): ReCaptchaFactory = FieldFactory( ReCaptcha, _(u'label_recaptcha_field', default=u'ReCaptcha')) ReCaptchaHandler = BaseHandler(ReCaptcha) + +@implementer(ILikert) +class Likert(TextLine): + """A Likert field""" + + def __init__(self, **kwargs): + self.answers = kwargs.get('answers', None) + if 'answers' in kwargs: + del kwargs['answers'] + self.questions = kwargs.get('questions', None) + if 'questions' in kwargs: + del kwargs['questions'] + Field.__init__(self, **kwargs) + + def _validate(self, value): + super(Likert, self)._validate(value) + self.parse(value) + + def parse(self, value): + result = dict() + lines = value.split(',') + for line in lines: + if not line: + continue + id, answer = line.split(':') + answer = answer.strip() + if answer not in self.answers: + raise ValueError('Invalid answer value.') + index = int(id) + if index < 1 or index > len(self.questions): + raise ValueError('Invalid question index.') + result[index] = answer + return result + + +LikertFactory = FieldFactory( + Likert, _(u"label_likert_field", default=u"Likert") +) +LikertHandler = BaseHandler(Likert) diff --git a/collective/easyform/fields.zcml b/collective/easyform/fields.zcml index 8285a728..ce38e0af 100644 --- a/collective/easyform/fields.zcml +++ b/collective/easyform/fields.zcml @@ -48,4 +48,12 @@ factory="plone.formwidget.recaptcha.ReCaptchaValidator" /> + + diff --git a/collective/easyform/interfaces.py b/collective/easyform/interfaces.py index 12adb01b..872da1f0 100644 --- a/collective/easyform/interfaces.py +++ b/collective/easyform/interfaces.py @@ -27,6 +27,7 @@ from plone.schemaeditor.interfaces import IFieldContext from plone.schemaeditor.interfaces import IFieldEditorExtender from plone.schemaeditor.interfaces import ISchemaContext +from plone.schemaeditor.schema import ITextLinesField from plone.supermodel.model import Schema from plone.supermodel.model import fieldset from plone.z3cform.interfaces import IFormWrapper @@ -53,6 +54,10 @@ from zope.schema.interfaces import ITextLine from zope.tales.tales import CompilerError +import zope.interface +import zope.schema.interfaces +import z3c.form.interfaces + try: from plone.schemaeditor import SchemaEditorMessageFactory as __ except ImportError: @@ -1087,3 +1092,25 @@ class IReCaptcha(ITextLine): class IFieldValidator(Interface): """Base marker for field validators""" + + +class ILikert(zope.schema.interfaces.IField): + + questions = zope.schema.List( + title=_(u'Possible questions'), + description=_(u'Enter allowed choices one per line.'), + required=zope.schema.interfaces.IChoice['vocabulary'].required, + default=zope.schema.interfaces.IChoice['vocabulary'].default, + value_type=zope.schema.TextLine()) + zope.interface.alsoProvides(questions, ITextLinesField) + + answers = zope.schema.List( + title=_(u'Possible answers'), + description=_(u'Enter allowed choices one per line.'), + required=zope.schema.interfaces.IChoice['vocabulary'].required, + default=zope.schema.interfaces.IChoice['vocabulary'].default, + value_type=zope.schema.TextLine()) + zope.interface.alsoProvides(questions, ITextLinesField) + +class ILikertWidget(z3c.form.interfaces.IWidget): + """Likert widget.""" diff --git a/collective/easyform/migration/fields.py b/collective/easyform/migration/fields.py index 5d404bef..66a3fa66 100644 --- a/collective/easyform/migration/fields.py +++ b/collective/easyform/migration/fields.py @@ -177,6 +177,7 @@ def to_text(value): ), "FormFileField": Type("plone.namedfile.field.NamedBlobFile", append_field), "FormCaptchaField": Type("collective.easyform.fields.ReCaptcha", append_field), + "FormLikertField": Type("collective.easyform.fields.Likert", append_field), "FieldsetStart": Type("", append_fieldset), "FieldsetEnd": Type("", None), } @@ -193,6 +194,8 @@ def to_text(value): "fgTVocabulary": None, # Not available in collective.easyform "fgVocabulary": Property("values", append_vocab_node), "hidden": Property("easyform:THidden", set_attribute), + "likertQuestions": Property("questions", append_list_node), + "likertAnswers": Property("answers", append_list_node), "maxval": Property("max", append_node), "minval": Property("min", append_node), "placeholder": None, # Not available in collective.easyform diff --git a/collective/easyform/tests/testLikert.py b/collective/easyform/tests/testLikert.py new file mode 100644 index 00000000..6fa102f4 --- /dev/null +++ b/collective/easyform/tests/testLikert.py @@ -0,0 +1,112 @@ +import unittest +import six + +from collective.easyform.tests.base import EasyFormTestCase + + +class LikertFieldTests(unittest.TestCase): + + def _getTargetClass(self): + from collective.easyform.fields import Likert + return Likert + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test_ctor_defaults(self): + txt = self._makeOne() + self.assertEqual(txt._type, six.text_type) + + def test_validate_not_required(self): + field = self._makeOne(required=False) + field.validate(None) + field.validate(u'') + + def test_validate_with_answers(self): + field = self._makeOne(required=False, questions=[u'Question 1'], answers=[u'Agree', u'Disagree']) + field.validate(None) + field.validate(u'') + field.validate(u'1: Agree') + self.assertRaises(ValueError, field.validate, u'1:agree') + self.assertRaises(ValueError, field.validate, u'-1:agree') + self.assertRaises(ValueError, field.validate, u'Agree') + + def test_validate_with_more_answers(self): + field = self._makeOne(required=False, questions=[u'Question 1', u'Question 2'], answers=[u'Agree', u'Disagree']) + field.validate(None) + field.validate(u'') + field.validate(u'1: Agree') + field.validate(u'2: Agree') + field.validate(u'1: Disagree, 2: Agree') + self.assertRaises(ValueError, field.validate, u'1:agree') + self.assertRaises(ValueError, field.validate, u'-1:agree') + self.assertRaises(ValueError, field.validate, u'Agree') + + def test_parse(self): + field = self._makeOne(required=False, questions=[u'Question 1', u'Question 2'], answers=[u'Agree', u'Disagree']) + field.validate(None) + self.assertEquals(dict(), field.parse(u'')) + self.assertEquals({1: u'Agree'}, field.parse(u'1: Agree')) + self.assertEquals({2: u'Agree'}, field.parse(u'2: Agree')) + self.assertEquals( + {1: u'Disagree', 2: u'Agree'}, + field.parse(u'1: Disagree, 2: Agree') + ) + + +class LikerWidgetTests(EasyFormTestCase): + + def test_likert(self): + self.folder.invokeFactory("EasyForm", "ff1") + ff1 = getattr(self.folder, "ff1") + self.assertEqual(ff1.portal_type, u'EasyForm') + + from zope.interface import Interface + from zope.schema import getFieldsInOrder + from collective.easyform.api import set_fields + from collective.easyform.api import set_actions + from collective.easyform.api import get_fields + from collective.easyform.actions import SaveData + from collective.easyform.fields import Likert + + class IWithLikert(Interface): + likert = Likert(questions=[u"Q1", u"Q2"], answers=[u"Agree", u"Disagree"]) + + set_fields(ff1, IWithLikert) + + schema = get_fields(ff1) + fields = getFieldsInOrder(schema) + self.assertEqual(len(fields), 1) + self.assertEqual(fields[0][0], 'likert') + self.assertTrue(isinstance(fields[0][1], Likert)) + + # check that LikertWidget is used + # and that questions and answers are rendered in a table + view = ff1.restrictedTraverse('view') + rendered = view() + self.assertTrue(u"likert-widget" in rendered) + self.assertTrue(u"Q1" in rendered) + self.assertTrue(u"Q2" in rendered) + self.assertTrue(u"Agree" in rendered) + self.assertTrue(u"Disagree" in rendered) + + class IWithSaver(Interface): + saver = SaveData(showFields=[]) + + IWithSaver.setTaggedValue('context', ff1) + set_actions(ff1, IWithSaver) + + # This part of the test does work on 4.x branch + # help is welcome to fix it for Plone 4.3 + + # submit data to test value extraction from widget + # ff1.CSRFProtection = False # no csrf protection + # view.request.form = { + # "form.buttons.submit": "Send", + # "form.widgets.likert.0": "Agree", + # "form.widgets.likert.1": "Disagree", + # } + # view.request.method = 'POST' + # rendered = view() + # self.assertTrue(u"Thank You" in rendered) + # self.assertTrue(u"1: Agree, 2: Disagree" in rendered) From 29a42aa367aeefdf3bc64b287ff0e549de15d1f4 Mon Sep 17 00:00:00 2001 From: Philip Bauer Date: Tue, 23 Aug 2022 19:41:32 +0200 Subject: [PATCH 11/14] handle flex fields --- collective/easyform/migration/fields.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/collective/easyform/migration/fields.py b/collective/easyform/migration/fields.py index 66a3fa66..8a60940c 100644 --- a/collective/easyform/migration/fields.py +++ b/collective/easyform/migration/fields.py @@ -280,6 +280,20 @@ def fields_model(ploneformgen): if name.startswith("fgT"): value = convert_tales_expressions(value) + if ( + type_.name == "zope.schema.Choice" + and value == "flex" + and len(properties.get("fgVocabulary", [])) > 4 + ): + # Flex is always migrated to RadioFieldWidget + # Here we switch to select if there are more than 4 options (same as the old flex option did) + value = "select" + logger.debug( + "Switch flex field %s to select in %s", + fieldname, + ploneformgen.absolute_url(), + ) + prop.handler(field, prop.name, value) if portal_type == "FieldsetStart": From a9d8f077c8ad55400ca7907182b420d0e15b868b Mon Sep 17 00:00:00 2001 From: Philip Bauer Date: Tue, 23 Aug 2022 19:42:57 +0200 Subject: [PATCH 12/14] fix py2/3 issue --- collective/easyform/migration/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index 51d2f19f..e9274e9d 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -45,7 +45,7 @@ def migrate_saved_data(ploneformgen, easyform): for key, value in zip(cols, row): try: field = schema.get(key) - value = value.decode("utf8") + value = safe_unicode(value) if IFromUnicode.providedBy(field) and value: value = field.fromUnicode(value) elif IDatetime.providedBy(field) and value: From c422796ad4421594d74181de714de34e09521af3 Mon Sep 17 00:00:00 2001 From: Philip Bauer Date: Tue, 23 Aug 2022 19:46:47 +0200 Subject: [PATCH 13/14] log exception on failed migration instead of aborting --- collective/easyform/migration/pfg.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/collective/easyform/migration/pfg.py b/collective/easyform/migration/pfg.py index 0f926764..897c461d 100644 --- a/collective/easyform/migration/pfg.py +++ b/collective/easyform/migration/pfg.py @@ -65,8 +65,11 @@ def migrate_ploneformgen(self): migrate_saved_data(self.old, self.new) def migrate(self, unittest=0): - super(PloneFormGenMigrator, self).migrate() - logger.info("Migrated FormFolder %s", "/".join(self.new.getPhysicalPath())) + try: + super(PloneFormGenMigrator, self).migrate() + logger.info("Migrated FormFolder %s", "/".join(self.new.getPhysicalPath())) + except Exception: + logger.info(u"Could not migrate %r", self.old, exc_info=True) def migrate_thankyou_page(self): pfg_thankspage = self.old.get(self.old.getThanksPage()) From 7e97a1b335a8eeea08388136008e2c3ca86f88ac Mon Sep 17 00:00:00 2001 From: Philip Bauer Date: Tue, 23 Aug 2022 19:48:22 +0200 Subject: [PATCH 14/14] Fix migrating saved data in set fields --- collective/easyform/migration/data.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/collective/easyform/migration/data.py b/collective/easyform/migration/data.py index e9274e9d..4fc532b0 100644 --- a/collective/easyform/migration/data.py +++ b/collective/easyform/migration/data.py @@ -53,7 +53,10 @@ def migrate_saved_data(ploneformgen, easyform): elif IDate.providedBy(field) and value: value = DateTime.DateTime(value).asdatetime().date() elif ISet.providedBy(field): - value = set(literal_eval(value)) + if not value or value == [""]: + value = set() + else: + value = set(value) elif INamedBlobFileField.providedBy(field): value = None except (