diff --git a/collective/watcherlist/actions/watching.py b/collective/watcherlist/actions/watching.py index 1ff4664..eb9d500 100644 --- a/collective/watcherlist/actions/watching.py +++ b/collective/watcherlist/actions/watching.py @@ -30,8 +30,8 @@ class IWatchingAction(interface.Interface): ) +@interface.implementer(IWatchingAction, IRuleElementData) class WatchingAction(SimpleItem): - interface.implements(IWatchingAction, IRuleElementData) watching = 'watch' name = '' @@ -39,8 +39,8 @@ class WatchingAction(SimpleItem): summary = _(u'Change if the user is in the watchers list or not.') +@interface.implementer(IExecutable) class WatchingActionExecutor(object): - interface.implements(IExecutable) adapts(interface.Interface, IWatchingAction, interface.Interface) def __init__(self, context, element, event): diff --git a/collective/watcherlist/browser.py b/collective/watcherlist/browser.py index 8b82911..befe23b 100644 --- a/collective/watcherlist/browser.py +++ b/collective/watcherlist/browser.py @@ -1,5 +1,5 @@ -from email.MIMEText import MIMEText -from email.MIMEMultipart import MIMEMultipart +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart from Products.Five.browser import BrowserView from collective.watcherlist import utils diff --git a/collective/watcherlist/event.py b/collective/watcherlist/event.py index d867962..973be25 100644 --- a/collective/watcherlist/event.py +++ b/collective/watcherlist/event.py @@ -7,16 +7,18 @@ class IAddedToWatchingEvent(IObjectEvent): """Event for when a user is added to the watchers list.""" +@interface.implementer(IAddedToWatchingEvent) class AddedToWatchingEvent(ObjectEvent): - interface.implements(IAddedToWatchingEvent) + """""" class IRemovedFromWatchingEvent(IObjectEvent): """Event for when a user is removed from the watchers list.""" +@interface.implementer(IRemovedFromWatchingEvent) class RemovedFromWatchingEvent(ObjectEvent): - interface.implements(IRemovedFromWatchingEvent) + """""" class IToggleWatchingEvent(IObjectEvent): @@ -24,5 +26,6 @@ class IToggleWatchingEvent(IObjectEvent): This event is sent before the add or remove events.""" +@interface.implementer(IToggleWatchingEvent) class ToggleWatchingEvent(ObjectEvent): - interface.implements(IToggleWatchingEvent) + """""" diff --git a/collective/watcherlist/mailer.py b/collective/watcherlist/mailer.py index 9c82836..fbc2c5f 100644 --- a/collective/watcherlist/mailer.py +++ b/collective/watcherlist/mailer.py @@ -39,10 +39,10 @@ def simple_send_mail(message, addresses, subject, immediate=False): "MailHost correctly.") # We print some info, which is perfect for checking in unit # tests. - print 'Subject =', subject - print 'Addresses =', addresses - print 'Message =' - print message + print('Subject =', subject) + print('Addresses =', addresses) + print('Message =') + print(message) return mfrom = utils.get_mail_from_address() diff --git a/collective/watcherlist/tests/__init__.py b/collective/watcherlist/tests/__init__.py deleted file mode 100644 index 792d600..0000000 --- a/collective/watcherlist/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/collective/watcherlist/tests/cornercases.txt b/collective/watcherlist/tests/cornercases.txt deleted file mode 100644 index cd5a88c..0000000 --- a/collective/watcherlist/tests/cornercases.txt +++ /dev/null @@ -1,223 +0,0 @@ -Testing corner cases -==================== - -Let's test some corner cases here that would otherwise clutter the -README.rst too much. - -First some setup:: - - >>> from zope.component import getGlobalSiteManager - >>> from zope.annotation.attribute import AttributeAnnotations - >>> from zope.annotation.interfaces import IAnnotations - >>> from collective.watcherlist.watchers import WatcherList - >>> class Dummy(object): - ... pass - >>> sm = getGlobalSiteManager() - >>> sm.registerAdapter(AttributeAnnotations, (Dummy, ), IAnnotations) - -With that out of the way, we create a dummy object and a watcherlist -for it:: - - >>> dummy = Dummy() - >>> wl = WatcherList(dummy) - >>> wl.addresses - () - -We take care to let the watchers always be a PersistentList:: - - >>> wl.watchers - [] - >>> type(wl.watchers) - - >>> wl.watchers = ['joe@example.org', 'mary@example.org'] - >>> wl.watchers - ['joe@example.org', 'mary@example.org'] - >>> type(wl.watchers) - - -The extra_addresses should also be persistent:: - - >>> wl.extra_addresses - [] - >>> type(wl.extra_addresses) - - >>> wl.extra_addresses = ['list@example.org'] - >>> wl.extra_addresses - ['list@example.org'] - >>> type(wl.extra_addresses) - - -The send_emails property should always be a boolean:: - - >>> wl.send_emails - True - >>> wl.send_emails = 0 - >>> wl.send_emails - False - >>> wl.addresses - () - >>> wl.send_emails = 1 - >>> wl.send_emails - True - >>> wl.addresses - ('list@example.org', 'mary@example.org', 'joe@example.org') - -We have no Plone Site setup, so no members can watch:: - - >>> wl.isWatching() - False - >>> wl.toggle_watching() - >>> wl.isWatching() - False - -Let's see if we can mock a portal_membership tool into these tests, -including mock users:: - - >>> from persistent.interfaces import IPersistent - >>> from zope.interface import implements - >>> class MockMemship(object): - ... implements(IPersistent) - ... current_member_name = None - ... members = [] - ... def isAnonymousUser(self): - ... return not bool(self.current_member_name) - ... def getMemberById(self, member_id): - ... for member in self.members: - ... if member.username == member_id: - ... return member - ... def getAuthenticatedMember(self): - ... return self.getMemberById(self.current_member_name) - ... def login(self, username): - ... self.current_member_name = username - ... def logout(self): - ... self.current_member_name = None - >>> memship = MockMemship() - >>> dummy.portal_membership = memship - -We add some members:: - - >>> class MockMember(object): - ... def __init__(self, username): - ... self.username = username - ... def getId(self): - ... return self.username - ... def getProperty(self, propname): - ... if propname == 'email': - ... return self.username + '@example.org' - >>> memship.members.append(MockMember('pete')) - >>> memship.members.append(MockMember('ann')) - -We check that anonymous and authenticated users get reported -correctly:: - - >>> dummy.portal_membership.isAnonymousUser() - True - >>> dummy.portal_membership.getAuthenticatedMember() - >>> memship.login('pete') - >>> dummy.portal_membership.isAnonymousUser() - False - >>> member = dummy.portal_membership.getAuthenticatedMember() - >>> member - - >>> member.username - 'pete' - >>> member.getProperty('email') - 'pete@example.org' - -We log out again:: - - >>> memship.logout() - >>> dummy.portal_membership.isAnonymousUser() - True - >>> dummy.portal_membership.getAuthenticatedMember() - -Let's try toggle watching again; as anonymous this still has no effect:: - - >>> wl.isWatching() - False - >>> wl.toggle_watching() - >>> wl.isWatching() - False - -Now toggle watching as authenticated member:: - - >>> memship.login('pete') - >>> wl.isWatching() - False - >>> wl.toggle_watching() - >>> wl.isWatching() - True - >>> wl.watchers - ['joe@example.org', 'mary@example.org', 'pete'] - >>> wl.addresses - ('list@example.org', 'mary@example.org', 'pete@example.org', 'joe@example.org') - >>> wl.toggle_watching() - >>> wl.watchers - ['joe@example.org', 'mary@example.org'] - >>> wl.addresses - ('list@example.org', 'mary@example.org', 'joe@example.org') - -In Products.Poi the issues have their own 'watchers' field. This -returns a tuple instead of a list. We respect that:: - - >>> class PoiLikeWatcherList(WatcherList): - ... _watchers = tuple() - ... def __get_watchers(self): - ... return self._watchers - ... def __set_watchers(self, v): - ... self._watchers = v - ... watchers = property(__get_watchers, __set_watchers) - >>> dummy2 = Dummy() - >>> poilist = PoiLikeWatcherList(dummy2) - >>> poilist.watchers - () - >>> dummy2.portal_membership = memship - >>> poilist.toggle_watching() - >>> poilist.watchers - ('pete',) - >>> poilist.addresses - ('pete@example.org',) - >>> poilist.toggle_watching() - >>> poilist.watchers - () - >>> poilist.addresses - () - -Let's test some utility functions a bit more. Get a safe unicode -version of a string or return unchanged; basically a small shim around -safe_unicode from Plone, with the default charset set correctly:: - - >>> from collective.watcherlist.utils import su - >>> su(None) - >>> su("hi") - u'hi' - >>> su(u"hi") - u'hi' - -Get the email of a member, with a few fallbacks:: - - >>> from collective.watcherlist.utils import get_member_email - >>> get_member_email(None, memship) - 'pete@example.org' - >>> get_member_email('ann', memship) - 'ann@example.org' - >>> get_member_email('no-one', memship) - >>> get_member_email('address@example.com', memship) - 'address@example.com' - -Allegedly, CMFMember can give some problems, so we check it:: - - >>> from AccessControl import Unauthorized - >>> class MockField(object): - ... def getAccessor(self, instance): - ... return lambda: instance.username + '@example.org' - >>> class MockCMFMember(MockMember): - ... def getProperty(self, propname): - ... if propname == 'email': - ... raise Unauthorized - ... def getField(self, name): - ... if name == 'email': - ... return MockField() - >>> memship.members.append(MockCMFMember('cmf')) - >>> get_member_email('cmf', memship) - 'cmf@example.org' diff --git a/collective/watcherlist/tests/test_integration.py b/collective/watcherlist/tests/test_integration.py deleted file mode 100644 index d6dc131..0000000 --- a/collective/watcherlist/tests/test_integration.py +++ /dev/null @@ -1,170 +0,0 @@ -from Acquisition import aq_base -from Products.CMFPlone.tests.utils import MockMailHost -from Products.Five import fiveconfigure -from Products.MailHost.interfaces import IMailHost -from Products.PloneTestCase import PloneTestCase as ptc -from Products.PloneTestCase.layer import PloneSite -from Testing import ZopeTestCase as ztc -from zope.component import getSiteManager - -# Sample implementation: -import collective.watcherlist -import collective.watcherlist.sample -import doctest -import unittest - -try: - # Plone 5 - from Products.CMFPlone.interfaces.controlpanel import IMailSchema - from plone.registry.interfaces import IRegistry - from zope.component import getUtility -except ImportError: - # Plone 4 and lower - IMailSchema = None - -try: - from Zope2.App import zcml - zcml # pyflakes -except ImportError: - from Products.Five import zcml -ptc.setupPloneSite() - -OPTIONFLAGS = (doctest.ELLIPSIS | - doctest.NORMALIZE_WHITESPACE) - - -class TestCase(ptc.PloneTestCase): - - class layer(PloneSite): - - @classmethod - def setUp(cls): - fiveconfigure.debug_mode = True - ztc.installPackage(collective.watcherlist) - # Load sample config: - zcml.load_config('', collective.watcherlist.sample) - fiveconfigure.debug_mode = False - - @classmethod - def tearDown(cls): - pass - - -class FunctionalTestCase(TestCase, ptc.FunctionalTestCase): - - def _setup(self): - ptc.PloneTestCase._setup(self) - # Replace normal mailhost with mock mailhost - self.portal._original_MailHost = self.portal.MailHost - self.portal.MailHost = mailhost = MockMailHost('MailHost') - sm = getSiteManager(context=self.portal) - sm.unregisterUtility(provided=IMailHost) - sm.registerUtility(mailhost, provided=IMailHost) - # Make sure our mock mailhost does not give a mailhost_warning - # in the overview-controlpanel. - self.configure_mail_host(u'mock', 'admin@example.com') - - def _clear(self, call_close_hook=0): - self.portal.MailHost = self.portal._original_MailHost - sm = getSiteManager(context=self.portal) - sm.unregisterUtility(provided=IMailHost) - sm.registerUtility(aq_base(self.portal._original_MailHost), - provided=IMailHost) - ptc.PloneTestCase._clear(self) - - def get_smtp_host(self): - if IMailSchema is None: - # Plone 4 - return self.portal.MailHost.smtp_host - else: - # Plone 5.0 and higher - registry = getUtility(IRegistry) - mail_settings = registry.forInterface( - IMailSchema, prefix='plone', check=False) - return mail_settings.smtp_host - - def configure_mail_host(self, smtp_host, address=None): - if IMailSchema is None: - # Plone 4 - self.portal.MailHost.smtp_host = smtp_host - if address is not None: - self.portal.email_from_address = address - else: - # Plone 5.0 and higher - registry = getUtility(IRegistry) - mail_settings = registry.forInterface( - IMailSchema, prefix='plone', check=False) - if not isinstance(smtp_host, unicode): - # must be unicode - smtp_host = smtp_host.decode('utf-8') - mail_settings.smtp_host = smtp_host - if address is not None: - if isinstance(address, unicode): - # must be ascii - address = address.encode('utf-8') - mail_settings.email_from_address = address - - def afterSetUp(self): - """Add some extra content and do some setup. - """ - # We need to do this as Manager: - self.setRoles(['Manager']) - - # Add some news items: - sample_text = "

Have I got news for you!

" - self.portal.news.invokeFactory( - 'News Item', 'first', title="First News", text=sample_text) - self.portal.news.invokeFactory( - 'News Item', 'second', title="Second News", text=sample_text) - - # Set fullname and email address of test user: - member = self.portal.portal_membership.getAuthenticatedMember() - member.setMemberProperties({'fullname': 'Test User', - 'email': 'testuser@example.com'}) - - # Add extra members: - self.addMember('maurits', 'Maurits van Rees', 'maurits@example.com') - self.addMember('reinout', 'Reinout van Rees', 'reinout@example.com') - - # Setup test browser: - try: - from Testing.testbrowser import Browser - Browser # pyflakes - except ImportError: - from Products.Five.testbrowser import Browser - self.browser = Browser() - self.browser.handleErrors = False - self.browser.addHeader('Accept-Language', 'en-US') - self.portal.error_log._ignored_exceptions = () - - # No more Manager: - self.setRoles([]) - - def addMember(self, username, fullname, email): - self.portal.portal_membership.addMember( - username, ptc.default_password, [], []) - member = self.portal.portal_membership.getMemberById(username) - member.setMemberProperties({'fullname': fullname, 'email': email}) - - def browser_login(self, user=None): - if not user: - user = ptc.default_user - self.browser.open(self.portal.absolute_url() + '/login_form') - self.browser.getLink('Log in').click() - self.browser.getControl(name='__ac_name').value = user - self.browser.getControl(name='__ac_password').value = \ - ptc.default_password - self.browser.getControl(name='submit').click() - - -def test_suite(): - return unittest.TestSuite([ - - ztc.FunctionalDocFileSuite( - 'newsletter.txt', package='collective.watcherlist.sample', - test_class=FunctionalTestCase), - - ]) - -if __name__ == '__main__': - unittest.main(defaultTest='test_suite') diff --git a/collective/watcherlist/tests/test_unit.py b/collective/watcherlist/tests/test_unit.py deleted file mode 100644 index d7cc0fd..0000000 --- a/collective/watcherlist/tests/test_unit.py +++ /dev/null @@ -1,27 +0,0 @@ -import unittest - -import doctest -from zope.component import testing - -OPTIONFLAGS = (doctest.ELLIPSIS | - doctest.NORMALIZE_WHITESPACE) - - -def test_suite(): - return unittest.TestSuite([ - - # Unit tests - doctest.DocFileSuite( - 'README.rst', package='collective.watcherlist', - setUp=testing.setUp, tearDown=testing.tearDown, - optionflags=OPTIONFLAGS), - - doctest.DocFileSuite( - 'cornercases.txt', package='collective.watcherlist.tests', - setUp=testing.setUp, tearDown=testing.tearDown, - optionflags=OPTIONFLAGS), - - ]) - -if __name__ == '__main__': - unittest.main(defaultTest='test_suite') diff --git a/collective/watcherlist/watchers.py b/collective/watcherlist/watchers.py index 0ed1bff..fbb4362 100644 --- a/collective/watcherlist/watchers.py +++ b/collective/watcherlist/watchers.py @@ -6,8 +6,7 @@ from persistent.list import PersistentList from zope.annotation.interfaces import IAnnotations from zope.event import notify -from zope.interface import implements -import sets +from zope.interface import implementer from collective.watcherlist.interfaces import IWatcherList from collective.watcherlist.mailer import simple_send_mail @@ -36,7 +35,7 @@ class WatcherList(object): """ - implements(IWatcherList) + implementer(IWatcherList) ANNO_KEY = 'collective.watcherlist' def __init__(self, context): @@ -186,7 +185,7 @@ def addresses(self): return () # make sure no duplicates are added - addresses = sets.Set() + addresses = set() context = aq_inner(self.context) memship = getToolByName(context, 'portal_membership', None) @@ -194,11 +193,11 @@ def addresses(self): # Okay, either we are in a simple unit test, or someone is # using this package outside of CMF/Plone. We should # assume the watchers are simple email addresses. - addresses.union_update(self.watchers) + addresses.update(self.watchers) else: - addresses.union_update([get_member_email(w, memship) + addresses.update([get_member_email(w, memship) for w in self.watchers]) - addresses.union_update(self.extra_addresses) + addresses.update(self.extra_addresses) # Discard invalid addresses: addresses.discard(None) @@ -210,7 +209,7 @@ def addresses(self): # Get addresses from parent (might be recursive). parent_list = IWatcherList(aq_parent(context), None) if parent_list is not None: - addresses.union_update(parent_list.addresses) + addresses.update(parent_list.addresses) return tuple(addresses) @@ -234,7 +233,7 @@ def send(self, view_name, only_these_addresses=None, **kw): if not addresses: logger.info("No addresses found.") return - if isinstance(addresses, basestring): + if isinstance(addresses, str): addresses = [addresses] immediate = kw.pop('immediate', False) diff --git a/setup.py b/setup.py index c675a34..3dec290 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ tests = open(os.path.join('collective', 'watcherlist', 'README.rst')).read() changes = open("CHANGES.rst").read() -version = '3.1.1.dev0' +version = '3.2.dev0' setup( name='collective.watcherlist', @@ -16,9 +16,12 @@ "Framework :: Plone", "Framework :: Plone :: 4.3", "Framework :: Plone :: 5.0", + "Framework :: Plone :: 5.1", + "Framework :: Plone :: 5.2", "License :: OSI Approved :: GNU General Public License (GPL)", "Programming Language :: Python", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.7", ], keywords='Plone notifications watching', author='Maurits van Rees',