Skip to content

Commit

Permalink
Merge pull request #37 from plone/python3
Browse files Browse the repository at this point in the history
migrate all tests to use dexterity, fix most tests
  • Loading branch information
pbauer authored Sep 17, 2018
2 parents bccf6ba + 8397b64 commit e1cb327
Show file tree
Hide file tree
Showing 28 changed files with 187 additions and 165 deletions.
9 changes: 8 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ New features:

Bug fixes:

- *add item here*
- Migrate all tests to use dexterity
[pbauer]

- Work around issue where new item is moved before it's completely addeed
[davisagli]

- Fix all tests with py3 and py2
[pbauer, alert, davisagli]


4.0.18 (2018-02-04)
Expand Down
8 changes: 4 additions & 4 deletions plone/app/contentrules/exportimport/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def export_field(self, doc, field):

if value is not None:
if ICollection.providedBy(field):
for e in value:
for e in sorted(value):
list_element = doc.createElement('element')
list_element.appendChild(doc.createTextNode(str(e)))
child.appendChild(list_element)
Expand Down Expand Up @@ -317,7 +317,7 @@ def _extractRules(self):

assignment_paths = set()

for name, rule in storage.items():
for name, rule in sorted(storage.items()):
rule_node = self._doc.createElement('rule')

rule_node.setAttribute('name', name)
Expand Down Expand Up @@ -367,7 +367,7 @@ def _extractRules(self):
# are orderd properly

site_path_length = len('/'.join(site.getPhysicalPath()))
for path in assignment_paths:
for path in sorted(assignment_paths):
try:
container = site.unrestrictedTraverse(path)
except KeyError:
Expand All @@ -378,7 +378,7 @@ def _extractRules(self):
continue

location = path[site_path_length:]
for name, assignment in assignable.items():
for name, assignment in sorted(assignable.items()):
assignment_node = self._doc.createElement('assignment')
assignment_node.setAttribute('location', location)
assignment_node.setAttribute('name', name)
Expand Down
30 changes: 30 additions & 0 deletions plone/app/contentrules/testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer

import plone.app.contentrules


class PloneAppContentrulesLayer(PloneSandboxLayer):

defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)

def setUpZope(self, app, configurationContext):
self.loadZCML('testing.zcml', package=plone.app.contentrules.tests)


PLONE_APP_CONTENTRULES_FIXTURE = PloneAppContentrulesLayer()


PLONE_APP_CONTENTRULES_INTEGRATION_TESTING = IntegrationTesting(
bases=(PLONE_APP_CONTENTRULES_FIXTURE,),
name='PloneAppContentrulesLayer:IntegrationTesting',
)


PLONE_APP_CONTENTRULES_FUNCTIONAL_TESTING = FunctionalTesting(
bases=(PLONE_APP_CONTENTRULES_FIXTURE,),
name='PloneAppContentrulesLayer:FunctionalTesting',
)
3 changes: 3 additions & 0 deletions plone/app/contentrules/tests/assignment.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ Setup
-----

>>> from plone.app.testing import SITE_OWNER_NAME, SITE_OWNER_PASSWORD
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID
>>> from plone.testing.z2 import Browser

>>> portal = layer['portal']
>>> setRoles(portal, TEST_USER_ID, ['Manager'])
>>> if 'news' not in layer['portal']:
... obj = portal.invokeFactory('Folder', 'news')
>>> import transaction
Expand Down
30 changes: 20 additions & 10 deletions plone/app/contentrules/tests/base.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
# -*- coding: utf-8 -*-
"""Base class for integration tests, based on plone.app.testing
"""

from plone.app.testing.bbb import PloneTestCase
from plone.app.contentrules.testing import PLONE_APP_CONTENTRULES_INTEGRATION_TESTING # noqa: E501
from plone.app.testing import login
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from zope.component import getMultiAdapter

import unittest


class ContentRulesTestCase(PloneTestCase):
class ContentRulesTestCase(unittest.TestCase):
"""Base class for integration tests for plone.app.contentrules.
This may provide specific set-up and tear-down operations, or provide
convenience methods.
"""

layer = PLONE_APP_CONTENTRULES_INTEGRATION_TESTING

def setUp(self):
self.portal = self.layer['portal']
self.request = self.layer['request']
login(self.portal, TEST_USER_NAME)
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal.invokeFactory('Folder', 'f1')
self.folder = self.portal['f1']
self.folder.invokeFactory('Document', 'd1')
self.portal.invokeFactory('Folder', 'target')

def addAuthToRequest(self):
portal = self.layer['portal']
request = self.layer['request']
authenticator = getMultiAdapter(
(portal, request), name=u'authenticator')
auth = authenticator.authenticator().split('value="')[1].rstrip('"/>')
request.form['_authenticator'] = auth


class ContentRulesFunctionalTestCase(PloneTestCase):
"""Base class for functional integration tests for plone.app.contentrules.
This may provide specific set-up and tear-down operations, or provide
convenience methods.
"""
14 changes: 10 additions & 4 deletions plone/app/contentrules/tests/multipublish.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ Setup

>>> from plone.app.testing import SITE_OWNER_NAME, SITE_OWNER_PASSWORD
>>> from plone.testing.z2 import Browser
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID

>>> portal = layer['portal']
>>> setRoles(portal, TEST_USER_ID, ['Manager'])
>>> obj = portal.invokeFactory('Folder', 'news')
>>> import transaction
>>> transaction.commit()

>>> browser = Browser(layer['app'])
>>> browser.addHeader('Authorization',
... 'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD))

>>> portal = layer['portal']

Let's visit the control panel and add a content rule. We'll add a
rule with a triggering event of `Workflow state changed`:

Expand Down Expand Up @@ -70,7 +76,7 @@ Let's go back and create two news items now:
>>> browser.getControl('Add').click()
>>> browser.getControl('Title').value = 'My news item'
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
>>> 'Item created' in browser.contents
True

>>> browser.getLink('Home').click()
Expand All @@ -79,7 +85,7 @@ Let's go back and create two news items now:
>>> browser.getControl('Add').click()
>>> browser.getControl('Title').value = 'Second news item'
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
>>> 'Item created' in browser.contents
True

Now let's publish both simultaneously.
Expand Down
7 changes: 6 additions & 1 deletion plone/app/contentrules/tests/simplepublish.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ Setup

>>> from plone.app.testing import SITE_OWNER_NAME, SITE_OWNER_PASSWORD
>>> from plone.testing.z2 import Browser
>>> from plone.app.testing import setRoles
>>> from plone.app.testing import TEST_USER_ID


>>> portal = layer['portal']
>>> setRoles(portal, TEST_USER_ID, ['Manager'])
>>> # portal.portal_workflow.setDefaultChain('simple_publication_workflow')
>>> if 'news' not in portal:
... obj = portal.invokeFactory('Folder', 'news')
>>> import transaction
Expand Down Expand Up @@ -70,7 +75,7 @@ Let's go back and create the news item now:
>>> browser.getControl('Add').click()
>>> browser.getControl('Title').value = 'My news item'
>>> browser.getControl('Save').click()
>>> 'Changes saved' in browser.contents
>>> 'Item created' in browser.contents
True
>>> browser.getLink('State:').click()
>>> ctrl = browser.getControl(name='workflow_action') # XXX fix label
Expand Down
15 changes: 5 additions & 10 deletions plone/app/contentrules/tests/test_action_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from plone.app.contentrules.actions.copy import CopyEditFormView
from plone.app.contentrules.rule import Rule
from plone.app.contentrules.tests.base import ContentRulesTestCase
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.contentrules.engine.interfaces import IRuleStorage
from plone.contentrules.rule.interfaces import IExecutable
Expand All @@ -22,12 +23,6 @@ def __init__(self, object):

class TestCopyAction(ContentRulesTestCase):

def afterSetUp(self):
self.loginAsPortalOwner()
self.portal.invokeFactory('Folder', 'target')
self.login()
self.folder.invokeFactory('Document', 'd1')

def testRegistered(self):
element = getUtility(IRuleAction, name='plone.actions.Copy')
self.assertEqual('plone.actions.Copy', element.addview)
Expand Down Expand Up @@ -84,7 +79,7 @@ def testExecuteWithError(self):
self.assertFalse('d1' in self.portal.target.objectIds())

def testExecuteWithoutPermissionsOnTarget(self):
self.setRoles(('Member', ))
setRoles(self.portal, TEST_USER_ID, ('Member', ))

e = CopyAction()
e.target_folder = '/target'
Expand All @@ -97,9 +92,9 @@ def testExecuteWithoutPermissionsOnTarget(self):
self.assertTrue('d1' in self.portal.target.objectIds())

def testExecuteWithNamingConflict(self):
self.setRoles(('Manager', ))
setRoles(self.portal, TEST_USER_ID, ('Manager', ))
self.portal.target.invokeFactory('Document', 'd1')
self.setRoles(('Member', ))
setRoles(self.portal, TEST_USER_ID, ('Member', ))

e = CopyAction()
e.target_folder = '/target'
Expand All @@ -122,7 +117,7 @@ def testExecuteWithNamingConflictDoesNotStupidlyAcquireHasKey(self):
self.folder.target.invokeFactory('Document', 'd1')

e = CopyAction()
e.target_folder = '/Members/{0}/target'.format(TEST_USER_ID)
e.target_folder = '/f1/target'

ex = getMultiAdapter(
(self.folder.target, e, DummyEvent(self.folder.d1)), IExecutable)
Expand Down
8 changes: 4 additions & 4 deletions plone/app/contentrules/tests/test_action_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
from zope.component import getUtility
from zope.component.interfaces import IObjectEvent
from zope.interface import implementer
from plone.app.testing import login
from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from plone.app.testing import setRoles


@implementer(IObjectEvent)
Expand All @@ -20,10 +24,6 @@ def __init__(self, object):

class TestDeleteAction(ContentRulesTestCase):

def afterSetUp(self):
self.setRoles(('Manager', ))
self.folder.invokeFactory('Document', 'd1')

def testRegistered(self):
element = getUtility(IRuleAction, name='plone.actions.Delete')
self.assertEqual('plone.actions.Delete', element.addview)
Expand Down
6 changes: 1 addition & 5 deletions plone/app/contentrules/tests/test_action_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ def __init__(self, obj):

class TestLoggerAction(ContentRulesTestCase):

def afterSetUp(self):
self.setRoles(('Manager', ))

def testRegistered(self):
element = getUtility(IRuleAction, name='plone.actions.Logger')
self.assertEqual('plone.actions.Logger', element.addview)
Expand Down Expand Up @@ -78,8 +75,7 @@ def testProcessedMessage(self):

e.message = 'Test log event : &c'
self.assertEqual(
'Test log event : '
'<ATFolder at /plone/Members/{0}>'.format(TEST_USER_ID),
'Test log event : <Folder at /plone/f1>',
ex.processedMessage(),
)

Expand Down
28 changes: 13 additions & 15 deletions plone/app/contentrules/tests/test_action_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from plone.app.contentrules.actions.mail import MailEditFormView
from plone.app.contentrules.rule import Rule
from plone.app.contentrules.tests.base import ContentRulesTestCase
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.contentrules.engine.interfaces import IRuleStorage
from plone.contentrules.rule.interfaces import IExecutable
from plone.contentrules.rule.interfaces import IRuleAction
Expand All @@ -31,11 +33,9 @@ def __init__(self, object):

class TestMailAction(ContentRulesTestCase):

def afterSetUp(self):
self.setRoles(('Manager', ))
self.portal.invokeFactory('Folder', 'target')
self.folder.invokeFactory('Document', 'd1',
title='W\xc3\xa4lkommen'.decode('utf-8'))
def setUp(self):
super(TestMailAction, self).setUp()
self.folder['d1'].setTitle(u'Wälkommen')

users = (
('userone', 'User One', '[email protected]', ('Manager', 'Member')),
Expand All @@ -47,6 +47,8 @@ def afterSetUp(self):
self.portal.portal_membership.addMember(id, 'secret', roles, [])
member = self.portal.portal_membership.getMemberById(id)
member.setMemberProperties({'fullname': fname, 'email': email})
# XXX: remove the manager role that was set in the base class
setRoles(self.portal, TEST_USER_ID, [])

def _setup_mockmail(self):
sm = getSiteManager(self.portal)
Expand Down Expand Up @@ -110,16 +112,14 @@ def testInvokeEditView(self):

def testExecute(self):
# this avoids sending mail as [email protected]
self.loginAsPortalOwner()
self.portal.portal_membership.getAuthenticatedMember().setProperties(
email='[email protected]')
dummyMailHost = self._setup_mockmail()
e = MailAction()
e.source = '$user_email'
e.recipients = '[email protected], [email protected], $reviewer_emails, ' \
'$manager_emails, $member_emails'
e.message = "P\xc3\xa4ge '${title}' created in ${url} !".decode(
'utf-8')
e.message = u"Päge '${title}' created in ${url} !"
ex = getMultiAdapter((self.folder, e, DummyEvent(self.folder.d1)),
IExecutable)
ex()
Expand All @@ -134,9 +134,8 @@ def testExecute(self):
self.assertEqual('[email protected]', mailSent.get('From'))
# The output message should be a utf-8 encoded string
self.assertEqual(
"P\xc3\xa4ge 'W\xc3\xa4lkommen' created in "
'http://nohost/plone/Members/test_user_1_/d1 !',
mailSent.get_payload(decode=True))
u"Päge 'Wälkommen' created in http://nohost/plone/f1/d1 !",
mailSent.get_payload(decode=True).decode('utf8'))

# check interpolation of $reviewer_emails
self.assertTrue('[email protected]' in sent_mails)
Expand Down Expand Up @@ -181,7 +180,7 @@ def testExecuteNoSource(self):
self.assertEqual('"plone@rulez" <[email protected]>',
mailSent.get('From'))
self.assertEqual('Document created !',
mailSent.get_payload(decode=True))
mailSent.get_payload())
self._teardown_mockmail()

def testExecuteMultiRecipients(self):
Expand All @@ -200,14 +199,14 @@ def testExecuteMultiRecipients(self):
self.assertEqual('[email protected]', mailSent.get('To'))
self.assertEqual('[email protected]', mailSent.get('From'))
self.assertEqual('Document created !',
mailSent.get_payload(decode=True))
mailSent.get_payload())
mailSent = message_from_string(dummyMailHost.messages[1])
self.assertEqual('text/plain; charset="utf-8"',
mailSent.get('Content-Type'))
self.assertEqual('[email protected]', mailSent.get('To'))
self.assertEqual('[email protected]', mailSent.get('From'))
self.assertEqual('Document created !',
mailSent.get_payload(decode=True))
mailSent.get_payload())
self._teardown_mockmail()

def testExecuteExcludeActor(self):
Expand Down Expand Up @@ -246,7 +245,6 @@ def testExecuteNoRecipients(self):
)
def testExecuteBadMailHost(self):
# Our goal is that mailing errors should not cause exceptions
self.loginAsPortalOwner()
self.portal.portal_membership.getAuthenticatedMember().setProperties(
email='[email protected]')
e = MailAction()
Expand Down
Loading

1 comment on commit e1cb327

@jenkins-plone-org
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pbauer Jenkins CI reporting about code analysis
See the full report here: https://jenkins.plone.org/job/package-plone.app.contentrules/53/violations

plone/app/contentrules/handlers.py:80:12: P002 found "hasattr", consider replacing it
plone/app/contentrules/handlers.py:82:12: P002 found "hasattr", consider replacing it
plone/app/contentrules/handlers.py:89:8: P002 found "hasattr", consider replacing it
plone/app/contentrules/handlers.py:92:8: P002 found "hasattr", consider replacing it
plone/app/contentrules/conditions/role.py:28:76: C812 missing trailing comma
plone/app/contentrules/conditions/role.py:46:59: C812 missing trailing comma
plone/app/contentrules/conditions/role.py:84:51: C812 missing trailing comma
plone/app/contentrules/conditions/role.py:105:51: C812 missing trailing comma
plone/app/contentrules/conditions/wftransition.py:28:68: C812 missing trailing comma
plone/app/contentrules/conditions/wftransition.py:29:10: C812 missing trailing comma
plone/app/contentrules/conditions/wfstate.py:28:63: C812 missing trailing comma
plone/app/contentrules/conditions/wfstate.py:29:10: C812 missing trailing comma
plone/app/contentrules/conditions/wfstate.py:46:59: C812 missing trailing comma
plone/app/contentrules/conditions/portaltype.py:33:72: C812 missing trailing comma
plone/app/contentrules/conditions/portaltype.py:34:10: C812 missing trailing comma
plone/app/contentrules/conditions/portaltype.py:60:50: C812 missing trailing comma
plone/app/contentrules/conditions/portaltype.py:79:16: P002 found "hasattr", consider replacing it
plone/app/contentrules/conditions/portaltype.py:98:34: C812 missing trailing comma
plone/app/contentrules/conditions/portaltype.py:119:26: C812 missing trailing comma
plone/app/contentrules/conditions/fileextension.py:31:22: C812 missing trailing comma
plone/app/contentrules/conditions/fileextension.py:49:50: C812 missing trailing comma
plone/app/contentrules/conditions/fileextension.py:90:78: C812 missing trailing comma
plone/app/contentrules/conditions/fileextension.py:113:78: C812 missing trailing comma
plone/app/contentrules/conditions/group.py:27:77: C812 missing trailing comma
plone/app/contentrules/conditions/group.py:45:60: C812 missing trailing comma
plone/app/contentrules/conditions/group.py:84:70: C812 missing trailing comma
plone/app/contentrules/conditions/group.py:105:70: C812 missing trailing comma
plone/app/contentrules/browser/elements.py:29:5: C901 'ManageElements.__call__' is too complex (12)
plone/app/contentrules/browser/elements.py:77:45: C812 missing trailing comma
plone/app/contentrules/browser/controlpanel.py:83:18: C812 missing trailing comma
plone/app/contentrules/browser/assignments.py:24:5: C901 'ManageAssignments.__call__' is too complex (14)
plone/app/contentrules/browser/assignments.py:154:54: C812 missing trailing comma
plone/app/contentrules/exportimport/rules.py:30:1: I004 isort found an unexpected blank line in imports
plone/app/contentrules/exportimport/rules.py:138:11: T000 Todo note found.
plone/app/contentrules/exportimport/rules.py:201:5: C901 'RulesXMLAdapter._initRules' is too complex (21)
plone/app/contentrules/tests/test_browser.py:2:80: E501 line too long (84 > 79 characters)
plone/app/contentrules/tests/test_action_logger.py:6:1: F401 'plone.app.testing.TEST_USER_ID' imported but unused
plone/app/contentrules/tests/test_rule_assignment_mapping.py:7:1: F401 'plone.app.contentrules.tests.base.ContentRulesTestCase' imported but unused
plone/app/contentrules/tests/test_cascading_rule.py:3:80: E501 line too long (84 > 79 characters)
plone/app/contentrules/tests/test_traversal.py:4:1: I001 isort found an import in the wrong position
plone/app/contentrules/tests/test_traversal.py:5:1: I001 isort found an import in the wrong position
plone/app/contentrules/tests/test_action_mail.py:50:11: T000 Todo note found.
plone/app/contentrules/tests/test_action_mail.py:95:14: C812 missing trailing comma
plone/app/contentrules/tests/test_action_mail.py:244:73: C812 missing trailing comma
plone/app/contentrules/tests/test_events.py:8:11: T000 Todo note found.
plone/app/contentrules/tests/test_events.py:12:11: T000 Todo note found.
plone/app/contentrules/tests/test_condition_wftransition.py:68:18: C812 missing trailing comma
plone/app/contentrules/tests/test_condition_wftransition.py:83:18: C812 missing trailing comma
plone/app/contentrules/tests/test_condition_wftransition.py:98:18: C812 missing trailing comma
plone/app/contentrules/tests/test_action_delete.py:12:1: F401 'plone.app.testing.login' imported but unused
plone/app/contentrules/tests/test_action_delete.py:12:1: I001 isort found an import in the wrong position
plone/app/contentrules/tests/test_action_delete.py:13:1: F401 'plone.app.testing.TEST_USER_ID' imported but unused
plone/app/contentrules/tests/test_action_delete.py:13:1: I001 isort found an import in the wrong position
plone/app/contentrules/tests/test_action_delete.py:14:1: F401 'plone.app.testing.TEST_USER_NAME' imported but unused
plone/app/contentrules/tests/test_action_delete.py:14:1: I001 isort found an import in the wrong position
plone/app/contentrules/tests/test_action_delete.py:15:1: F401 'plone.app.testing.setRoles' imported but unused
plone/app/contentrules/tests/test_action_delete.py:15:1: I001 isort found an import in the wrong position
plone/app/contentrules/tests/test_configuration.py:90:80: E501 line too long (85 > 79 characters)
plone/app/contentrules/tests/test_action_workflow.py:70:14: C812 missing trailing comma
plone/app/contentrules/tests/test_action_workflow.py:89:14: C812 missing trailing comma
plone/app/contentrules/actions/copy.py:85:45: C812 missing trailing comma
plone/app/contentrules/actions/copy.py:86:18: C812 missing trailing comma
plone/app/contentrules/actions/copy.py:126:56: C812 missing trailing comma
plone/app/contentrules/actions/move.py:54:52: C812 missing trailing comma
plone/app/contentrules/actions/move.py:91:18: C812 missing trailing comma
plone/app/contentrules/actions/move.py:144:56: C812 missing trailing comma
plone/app/contentrules/actions/logger.py:22:60: C812 missing trailing comma
plone/app/contentrules/actions/logger.py:50:10: C812 missing trailing comma
plone/app/contentrules/actions/logger.py:94:18: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:39:22: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:45:57: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:47:23: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:53:67: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:55:22: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:59:80: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:64:22: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:102:5: C901 'MailActionExecutor.__call__' is too complex (13)
plone/app/contentrules/actions/mail.py:106:74: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:129:78: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:152:19: C812 missing trailing comma
plone/app/contentrules/actions/mail.py:165:19: T000 Todo note found.
plone/app/contentrules/actions/mail.py:176:78: C812 missing trailing comma

Follow these instructions to reproduce it locally.

Please sign in to comment.