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/actions.py b/collective/easyform/migration/actions.py
index c251ff1c..7fa1064e 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,49 @@ 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_list_node),
+ "ExtraData": Property("ExtraData", append_list_node),
+ "DownloadFormat": Property("DownloadFormat", append_node),
+ "UseColumnNames": Property("UseColumnNames", append_node),
+ "ProxyRole": Property("ProxyRole", append_node),
+ "ScriptBody": Property("ScriptBody", append_node),
}
@@ -86,7 +88,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 +101,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)
@@ -122,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 3ee7bfa0..4fc532b0 100644
--- a/collective/easyform/migration/data.py
+++ b/collective/easyform/migration/data.py
@@ -3,50 +3,100 @@
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 Products.CMFPlone.utils import safe_unicode
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
-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)
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', idx,
- '/'.join(easyform.getPhysicalPath()),
- data_adapter.getId())
+ 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,
+ )
continue
data = {}
for key, value in zip(cols, row):
- field = schema.get(key)
- value = value.decode('utf8')
- if IFromUnicode.providedBy(field):
- 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:
+ field = schema.get(key)
+ value = safe_unicode(value)
+ 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):
+ if not value or value == [""]:
+ value = set()
+ else:
+ value = set(value)
+ elif INamedBlobFileField.providedBy(field):
+ value = None
+ except (
+ ValueError,
+ TypeError,
+ 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...
+ # Older datarows can break in these cases
+
try:
- value = set(literal_eval(value))
- except ValueError:
- pass
- elif INamedBlobFileField.providedBy(field):
- value = None
+ 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.".format(
+ key,
+ value_to_log,
+ "/".join(easyform.getPhysicalPath()),
+ data_adapter.getId(),
+ )
+ )
+ logger.warning(
+ "BEWARE: to keep data integrity, the data was not migrated for "
+ "data adapter %s/%s.",
+ "/".join(easyform.getPhysicalPath()),
+ data_adapter.getId(),
+ )
+ action.clearSavedFormInput()
+ return
data[key] = value
action.addDataRow(data)
diff --git a/collective/easyform/migration/fields.py b/collective/easyform/migration/fields.py
index f6208537..8a60940c 100644
--- a/collective/easyform/migration/fields.py
+++ b/collective/easyform/migration/fields.py
@@ -5,63 +5,64 @@
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
-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 +70,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
@@ -147,61 +149,67 @@ 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
-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),
+ "FormLikertField": Type("collective.easyform.fields.Likert", 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),
+ "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
+ "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 +245,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 +258,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 +277,26 @@ 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)
+ 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':
+ 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..897c461d 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):
@@ -58,29 +60,49 @@ 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()))
+ 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())
+ 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(
- 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 +111,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 +129,21 @@ 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()
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)