diff --git a/.travis.yml b/.travis.yml index 78c862fb..0af7a537 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,10 @@ jobs: env: PLONE_VERSION=5.2.x dist: bionic sudo: true + - python: "3.8" + env: PLONE_VERSION=5.2.3-pending + dist: bionic + sudo: true cache: pip: true diff --git a/news/252.bugfix b/news/252.bugfix new file mode 100644 index 00000000..967c11de --- /dev/null +++ b/news/252.bugfix @@ -0,0 +1,3 @@ +Fix validators in field sets with zope.interface 5.1+. +This fixes `issue 252 `_. +[maurits] diff --git a/src/collective/easyform/fields.py b/src/collective/easyform/fields.py index d356db89..4f536f6f 100644 --- a/src/collective/easyform/fields.py +++ b/src/collective/easyform/fields.py @@ -29,11 +29,24 @@ def superAdapter(specific_interface, adapter, objects, name=u""): """Find the next most specific adapter. - """ - # We are adjusting view object class to provide IForm rather than IEasyFormForm or IGroup to make - # one of the objects less specific. This allows us to find anotehr adapter other than our one. This allows us to - # find any custom adapters for any fields that we have overridden + This is called by a FieldExtenderValidator or FieldExtenderDefault instance. + This is passed in with the 'adapter' parameter. + This adapter itself is not a real validator or default factory, + but is used to find other real validators or default factories. + + This may sound strange, but it solves a problem. + Problem is that validators and default were not always found for fields in field sets. + For example, a captcha field in the main form would get validated by its proper validator, + but when in a field set, only a basic validator would be found. + + We are adjusting the view object class to provide IForm rather than + IEasyFormForm or IGroup to make one of the objects less specific. + Same for the default factory. + This allows us to find another adapter other than the current one. + This allows us to find any custom adapters for any fields that we have overridden + + """ new_obj = [] found = False for obj in objects: @@ -59,11 +72,18 @@ def __getattr__(self, item): if not found: return None - provided_by_declared = providedBy(adapter).declared - if not provided_by_declared: - return None + provided_by = providedBy(adapter) + # With zope.interface 5.0.2, the info we seek is in 'declared'. + # With 5.1.0+, it can also be in the 'interfaces()' iterator, + # especially for groups (field sets). + # But it looks like interfaces() works always. + # adapter_interfaces = provided_by.declared + # if not adapter_interfaces: + adapter_interfaces = list(provided_by.interfaces()) + if not adapter_interfaces: + return - return queryMultiAdapter(new_obj, provided_by_declared[0], name=name) + return queryMultiAdapter(new_obj, adapter_interfaces[0], name=name) @implementer(IValidator) diff --git a/src/collective/easyform/tests/base.py b/src/collective/easyform/tests/base.py index 5f2f0a14..7683b0df 100644 --- a/src/collective/easyform/tests/base.py +++ b/src/collective/easyform/tests/base.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import print_function -from email import message_from_string from plone import api from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE @@ -21,12 +20,22 @@ except ImportError: from plone.testing.z2 import ZSERVER_FIXTURE as WSGI_SERVER_FIXTURE +try: + # Python 3 + from email import message_from_bytes +except ImportError: + # Python 2 + from email import message_from_string as message_from_bytes + class MailHostMock(MailHost): def _send(self, mfrom, mto, messageText, immediate=False): print("".format(mfrom, mto)) # noqa: T003 + if hasattr(messageText, "encode"): + # It is text instead of bytes. + messageText = messageText.encode("utf-8") self.msgtext = messageText - self.msg = message_from_string(messageText.lstrip()) + self.msg = message_from_bytes(messageText.lstrip()) class Fixture(PloneSandboxLayer): diff --git a/src/collective/easyform/tests/serverside_field.rst b/src/collective/easyform/tests/serverside_field.rst index 8a8ab83d..08d160ef 100644 --- a/src/collective/easyform/tests/serverside_field.rst +++ b/src/collective/easyform/tests/serverside_field.rst @@ -44,9 +44,9 @@ thank you page:: Test for 'Subject' in the mail body:: - >>> msgtext = portal.MailHost.msgtext[portal.MailHost.msgtext.index('\n\n'):] - >>> body = '\n\n'.join(portal.MailHost.msgtext.split('\n\n')[1:]) - >>> 'Subject' in body + >>> msgtext = portal.MailHost.msgtext[portal.MailHost.msgtext.index(b'\n\n'):] + >>> body = b'\n\n'.join(portal.MailHost.msgtext.split(b'\n\n')[1:]) + >>> b'Subject' in body False Specifically list the field as one that should be included in the thank diff --git a/src/collective/easyform/tests/testMailer.py b/src/collective/easyform/tests/testMailer.py index 043dcac4..4a66e6af 100644 --- a/src/collective/easyform/tests/testMailer.py +++ b/src/collective/easyform/tests/testMailer.py @@ -10,13 +10,20 @@ from collective.easyform.api import set_fields from collective.easyform.interfaces import IActionExtender from collective.easyform.tests import base +from email.header import decode_header from plone import api from plone.app.textfield.value import RichTextValue from plone.namedfile.file import NamedFile from Products.CMFPlone.utils import safe_unicode import datetime -import email + +try: + # Python 3 + from email import message_from_bytes +except ImportError: + # Python 2 + from email import message_from_string as message_from_bytes class TestFunctions(base.EasyFormTestCase): @@ -25,8 +32,11 @@ class TestFunctions(base.EasyFormTestCase): def dummy_send(self, mfrom, mto, messageText, immediate=False): self.mfrom = mfrom self.mto = mto + if hasattr(messageText, "encode"): + # It is text instead of bytes. + messageText = messageText.encode("utf-8") self.messageText = messageText - self.messageBody = "\n\n".join(messageText.split("\n\n")[1:]) + self.messageBody = b"\n\n".join(messageText.split(b"\n\n")[1:]) def afterSetUp(self): super(TestFunctions, self).afterSetUp() @@ -53,11 +63,11 @@ def test_DummyMailer(self): self.mailhost.send( "messageText", mto="dummy@address.com", mfrom="dummy1@address.com" ) - self.assertTrue(self.messageText.endswith("messageText")) + self.assertTrue(self.messageText.endswith(b"messageText")) self.assertEqual(self.mto, ["dummy@address.com"]) - self.assertIn("To: dummy@address.com", self.messageText) + self.assertIn(b"To: dummy@address.com", self.messageText) self.assertEqual(self.mfrom, "dummy1@address.com") - self.assertIn("From: dummy1@address.com", self.messageText) + self.assertIn(b"From: dummy1@address.com", self.messageText) def test_Mailer_Basic(self): """ Test mailer with dummy_send """ @@ -69,9 +79,9 @@ def test_Mailer_Basic(self): mailer.onSuccess(data, request) - self.assertIn("To: mdummy@address.com", self.messageText) - self.assertIn("Subject: =?utf-8?q?test_subject?=", self.messageText) - msg = email.message_from_string(self.messageText) + self.assertIn(b"To: mdummy@address.com", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?test_subject?=", self.messageText) + msg = message_from_bytes(self.messageText) self.assertIn("test comments", msg.get_payload(decode=False)) def test_MailerAdditionalHeaders(self): @@ -86,11 +96,11 @@ def test_MailerAdditionalHeaders(self): mailer.onSuccess(data, request) - self.assertIn("Generator: Plone", self.messageText) - self.assertIn("Token: abc", self.messageText) - self.assertIn("To: mdummy@address.com", self.messageText) - self.assertIn("Subject: =?utf-8?q?test_subject?=", self.messageText) - msg = email.message_from_string(self.messageText) + self.assertIn(b"Generator: Plone", self.messageText) + self.assertIn(b"Token: abc", self.messageText) + self.assertIn(b"To: mdummy@address.com", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?test_subject?=", self.messageText) + msg = message_from_bytes(self.messageText) self.assertIn("test comments", msg.get_payload(decode=False)) def test_MailerLongSubject(self): @@ -107,9 +117,9 @@ def test_MailerLongSubject(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - msg = email.message_from_string(self.messageText) + msg = message_from_bytes(self.messageText) encoded_subject_header = msg["subject"] - decoded_header = email.header.decode_header(encoded_subject_header)[0][0] + decoded_header = decode_header(encoded_subject_header)[0][0] self.assertEqual(decoded_header, long_subject.encode("utf-8")) @@ -128,7 +138,7 @@ def test_SubjectDollarReplacement(self): request = self.LoadRequestForm(**data) self.messageText = "" mailer.onSuccess(data, request) - self.assertIn("Subject: =?utf-8?q?test_subject?=", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?test_subject?=", self.messageText) data2 = dict( topic="test ${subject}", replyto="test@test.org", comments="test comments" @@ -138,7 +148,7 @@ def test_SubjectDollarReplacement(self): request = self.LoadRequestForm(**data2) self.messageText = "" mailer.onSuccess(data2, request) - self.assertIn("Subject: =?utf-8?q?test_=24=7Bsubject=7D?=", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?test_=24=7Bsubject=7D?=", self.messageText) # we should get substitution in a basic override mailer.subject_field = "" @@ -146,20 +156,20 @@ def test_SubjectDollarReplacement(self): self.messageText = "" mailer.onSuccess(data, request) self.assertIn( - "Subject: =?utf-8?q?This_is_my_test_subject_now?=", self.messageText + b"Subject: =?utf-8?q?This_is_my_test_subject_now?=", self.messageText ) # we should get substitution in a basic override mailer.msg_subject = "This is my ${untopic} now" - self.messageText = "" + self.messageText = b"" mailer.onSuccess(data, request) - self.assertIn("Subject: =?utf-8?q?This_is_my_=3F=3F=3F_now?=", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?This_is_my_=3F=3F=3F_now?=", self.messageText) # we don't want substitution on user input request = self.LoadRequestForm(**data2) - self.messageText = "" + self.messageText = b"" mailer.onSuccess(data2, request) - self.assertIn("Subject: =?utf-8?q?This_is_my_=3F=3F=3F_now?=", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?This_is_my_=3F=3F=3F_now?=", self.messageText) def test_TemplateReplacement(self): """ @@ -178,11 +188,11 @@ def test_TemplateReplacement(self): # we should get substitution request = self.LoadRequestForm(**data) - self.messageText = "" + self.messageText = b"" mailer.onSuccess(data, request) - self.assertIn("Hello test subject,", self.messageBody) - self.assertIn("Thanks, test subject!", self.messageBody) - self.assertIn("Eat my footer, test subject.", self.messageBody) + self.assertIn(b"Hello test subject,", self.messageBody) + self.assertIn(b"Thanks, test subject!", self.messageBody) + self.assertIn(b"Eat my footer, test subject.", self.messageBody) def test_UTF8Subject(self): """ Test mailer with uft-8 encoded subject line """ @@ -195,9 +205,9 @@ def test_UTF8Subject(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - msg = email.message_from_string(self.messageText) + msg = message_from_bytes(self.messageText) encoded_subject_header = msg["subject"] - decoded_header = email.header.decode_header(encoded_subject_header)[0][0] + decoded_header = decode_header(encoded_subject_header)[0][0] self.assertEqual(safe_unicode(decoded_header), utf8_subject) @@ -211,9 +221,9 @@ def test_UnicodeSubject(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - msg = email.message_from_string(self.messageText) + msg = message_from_bytes(self.messageText) encoded_subject_header = msg["subject"] - decoded_header = email.header.decode_header(encoded_subject_header)[0][0] + decoded_header = decode_header(encoded_subject_header)[0][0] self.assertEqual(safe_unicode(decoded_header), utf8_subject) @@ -225,9 +235,9 @@ def test_Utf8ListSubject(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - msg = email.message_from_string(self.messageText) + msg = message_from_bytes(self.messageText) encoded_subject_header = msg["subject"] - decoded_header = email.header.decode_header(encoded_subject_header)[0][0] + decoded_header = decode_header(encoded_subject_header)[0][0] self.assertEqual(safe_unicode(decoded_header), ", ".join(utf8_subject_list)) @@ -242,9 +252,9 @@ def test_MailerOverrides(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - self.assertIn("Subject: =?utf-8?q?eggs_and_spam?=", self.messageText) - self.assertIn("From: spam@eggs.com", self.messageText) - self.assertIn("To: eggs@spam.com", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?eggs_and_spam?=", self.messageText) + self.assertIn(b"From: spam@eggs.com", self.messageText) + self.assertIn(b"To: eggs@spam.com", self.messageText) def test_MailerOverridesWithFieldValues(self): mailer = get_actions(self.ff1)["mailer"] @@ -254,8 +264,8 @@ def test_MailerOverridesWithFieldValues(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - self.assertIn("Subject: =?utf-8?q?eggs_and_spam?=", self.messageText) - self.assertIn("To: test@test.ts", self.messageText) + self.assertIn(b"Subject: =?utf-8?q?eggs_and_spam?=", self.messageText) + self.assertIn(b"To: test@test.ts", self.messageText) def testMultiRecipientOverrideByString(self): """ try multiple recipients in recipient override """ @@ -267,7 +277,7 @@ def testMultiRecipientOverrideByString(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - self.assertIn("To: eggs@spam.com, spam@spam.com", self.messageText) + self.assertIn(b"To: eggs@spam.com, spam@spam.com", self.messageText) def testMultiRecipientOverrideByTuple(self): """ try multiple recipients in recipient override """ @@ -279,7 +289,7 @@ def testMultiRecipientOverrideByTuple(self): request = self.LoadRequestForm(**data) mailer.onSuccess(data, request) - self.assertIn("To: eggs@spam.com, spam.spam.com", self.messageText) + self.assertIn(b"To: eggs@spam.com, spam.spam.com", self.messageText) def testRecipientFromRequest(self): """ try recipient from designated field """ @@ -293,7 +303,7 @@ def testRecipientFromRequest(self): request = self.LoadRequestForm(**fields) mailer.onSuccess(fields, request) - self.assertIn("To: eggs@spamandeggs.com", self.messageText) + self.assertIn(b"To: eggs@spamandeggs.com", self.messageText) fields = { "topic": "test subject", @@ -302,7 +312,7 @@ def testRecipientFromRequest(self): request = self.LoadRequestForm(**fields) mailer.onSuccess(fields, request) - self.assertTrue(self.messageText.find("To: eggs@spam.com, spam@spam.com") > 0) + self.assertTrue(self.messageText.find(b"To: eggs@spam.com, spam@spam.com") > 0) def setExecCondition(self, value): actions = get_actions(self.ff1) @@ -323,23 +333,23 @@ def test_ExecConditions(self): ) self.LoadRequestForm(**fields) - self.messageText = "" + self.messageText = b"" self.setExecCondition("python: False") form.processActions(fields) self.assertTrue(len(self.messageText) == 0) - self.messageText = "" + self.messageText = b"" self.setExecCondition("python: True") form.processActions(fields) self.assertTrue(len(self.messageText) > 0) - self.messageText = "" + self.messageText = b"" self.setExecCondition("python: 1==0") form.processActions(fields) self.assertTrue(len(self.messageText) == 0) # make sure an empty execCondition causes the action to fire - self.messageText = "" + self.messageText = b"" self.setExecCondition("") form.processActions(fields) self.assertTrue(len(self.messageText) > 0) @@ -354,32 +364,32 @@ def test_selectiveFieldMailing(self): request = self.LoadRequestForm(**fields) # make sure all fields are sent unless otherwise specified - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertTrue( - "te=\nst subject" in self.messageBody - and "test@test.org" in self.messageBody - and "test comments" in self.messageBody + b"te=\nst subject" in self.messageBody + and b"test@test.org" in self.messageBody + and b"test comments" in self.messageBody ) # setting some show fields shouldn't change that mailer.showFields = ("topic", "comments") - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertTrue( - "te=\nst subject" in self.messageBody - and "test@test.org" in self.messageBody - and "test comments" in self.messageBody + b"te=\nst subject" in self.messageBody + and b"test@test.org" in self.messageBody + and b"test comments" in self.messageBody ) # until we turn off the showAll flag mailer.showAll = False - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertTrue( - "te=\nst subject" in self.messageBody - and "test@test.org" not in self.messageBody - and "test comments" in self.messageBody + b"te=\nst subject" in self.messageBody + and b"test@test.org" not in self.messageBody + and b"test comments" in self.messageBody ) # check includeEmpties @@ -387,13 +397,13 @@ def test_selectiveFieldMailing(self): # first see if everything's still included mailer.showAll = True - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) # look for labels self.assertTrue( - self.messageBody.find("Subject") > 0 - and self.messageBody.find("Your E-Mail Address") > 0 - and self.messageBody.find("Comments") > 0 + self.messageBody.find(b"Subject") > 0 + and self.messageBody.find(b"Your E-Mail Address") > 0 + and self.messageBody.find(b"Comments") > 0 ) # now, turn off required for a field and leave it empty @@ -402,11 +412,11 @@ def test_selectiveFieldMailing(self): set_fields(self.ff1, fields) fields = {"topic": "test subject", "replyto": "test@test.org"} request = self.LoadRequestForm(**fields) - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) - self.assertIn("Subject", self.messageBody) - self.assertIn("Your E-Mail Address", self.messageBody) - self.assertNotIn("Comments", self.messageBody) + self.assertIn(b"Subject", self.messageBody) + self.assertIn(b"Your E-Mail Address", self.messageBody) + self.assertNotIn(b"Comments", self.messageBody) def test_ccOverride(self): """ Test override for CC field """ @@ -417,19 +427,19 @@ def test_ccOverride(self): ) request = self.LoadRequestForm(**fields) mailer.cc_recipients = "test@testme.com" - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertIn("test@testme.com", self.mto) # simple override mailer.ccOverride = "string:test@testme.com" - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertIn("test@testme.com", self.mto) # list override mailer.ccOverride = "python:['test@testme.com', 'test1@testme.com']" - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertTrue( "test@testme.com" in self.mto and "test1@testme.com" in self.mto @@ -443,19 +453,19 @@ def test_bccOverride(self): ) request = self.LoadRequestForm(**fields) mailer.bcc_recipients = "test@testme.com" - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertIn("test@testme.com", self.mto) # simple override mailer.bccOverride = "string:test@testme.com" - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertIn("test@testme.com", self.mto) # list override mailer.bccOverride = "python:['test@testme.com', 'test1@testme.com']" - self.messageText = "" + self.messageText = b"" mailer.onSuccess(fields, request) self.assertTrue( "test@testme.com" in self.mto and "test1@testme.com" in self.mto @@ -476,7 +486,7 @@ def testNoRecipient(self): request = self.LoadRequestForm(**fields) - self.messageText = "" + self.messageText = b"" self.assertRaises(ValueError, mailer.onSuccess, fields, request) def test_custom_email_template(self): @@ -488,7 +498,7 @@ def test_custom_email_template(self): mailer = get_actions(self.ff1)["mailer"] mailer.onSuccess({}, self.layer["request"]) - self.assertIn(u"Custom e-mail template!", self.messageText) + self.assertIn(b"Custom e-mail template!", self.messageText) def test_MailerXMLAttachments(self): """ Test mailer with dummy_send """ diff --git a/tests-5.2.3-pending.cfg b/tests-5.2.3-pending.cfg new file mode 100644 index 00000000..237e7158 --- /dev/null +++ b/tests-5.2.3-pending.cfg @@ -0,0 +1,43 @@ +[buildout] +extends = + https://github.com/raw/collective/buildout.plonetest/master/test-5.2.x.cfg + https://dist.plone.org/release/5.2.3-pending/versions.cfg + https://github.com/raw/collective/buildout.plonetest/master/qa.cfg + https://github.com/raw/plone/plone.app.robotframework/master/versions.cfg + base.cfg + +parts += + createcoverage + +package-name = collective.easyform +package-extras = [test] +test-eggs = + +# Python3 compatibility for plone.formwidget.z3cform is not released +extensions += + mr.developer +sources = sources +sources-dir = share +auto-checkout += + plone.formwidget.recaptcha + +[versions] +setuptools = +zc.buildout = +coverage = >=3.7 +plone.app.mosaic = +plone.app.robotframework = 1.5.0 +# check-manifest = 0.45 requires 'build' 0.1.0. +# This requires a newer 'pep517' than is pinned by Plone, +# and on Python 2.7 a newer virtualenv than is pinned by Plone. +check-manifest = 0.44 + +[versions:python27] +check-manifest = 0.41 + +[sources] +plone.formwidget.recaptcha = git ${remotes:plone}/plone.formwidget.recaptcha.git pushurl=${remotes:plone_push}/plone.formwidget.recaptcha.git + +[remotes] +plone = https://github.com/plone +plone_push = git@github.com:plone diff --git a/tests-5.2.x.cfg b/tests-5.2.x.cfg index db703c63..3e7a7a26 100644 --- a/tests-5.2.x.cfg +++ b/tests-5.2.x.cfg @@ -26,6 +26,13 @@ zc.buildout = coverage = >=3.7 plone.app.mosaic = plone.app.robotframework = 1.5.0 +# check-manifest = 0.45 requires 'build' 0.1.0. +# This requires a newer 'pep517' than is pinned by Plone, +# and on Python 2.7 a newer virtualenv than is pinned by Plone. +check-manifest = 0.44 + +[versions:python27] +check-manifest = 0.41 [sources] plone.formwidget.recaptcha = git ${remotes:plone}/plone.formwidget.recaptcha.git pushurl=${remotes:plone_push}/plone.formwidget.recaptcha.git