diff --git a/CHANGES.rst b/CHANGES.rst
index 555f0ee0..153bec5e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,10 +1,11 @@
Changelog
=========
-4.0.6 (unreleased)
+4.1.0 (unreleased)
------------------
-- Nothing changed yet.
+- Add subscriber sub-template
+ [MrTango]
4.0.5 (2019-05-22)
diff --git a/bobtemplates/plone/subscriber.py b/bobtemplates/plone/subscriber.py
new file mode 100644
index 00000000..26cd507d
--- /dev/null
+++ b/bobtemplates/plone/subscriber.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+
+from bobtemplates.plone.base import base_prepare_renderer
+from bobtemplates.plone.base import git_commit
+from bobtemplates.plone.base import remove_unwanted_files
+from bobtemplates.plone.base import update_configure_zcml
+
+import case_conversion as cc
+
+
+def _update_package_configure_zcml(configurator):
+ path = '{0}'.format(
+ configurator.variables['package_folder'],
+ )
+ file_name = u'configure.zcml'
+ match_xpath = "include[@package='.subscribers']"
+ match_str = '-*- extra stuff goes here -*-'
+ insert_str = """
+
+"""
+ update_configure_zcml(
+ configurator,
+ path,
+ file_name=file_name,
+ match_xpath=match_xpath,
+ match_str=match_str,
+ insert_str=insert_str,
+ )
+
+
+def _update_subscribers_configure_zcml(configurator):
+ path = '{0}/subscribers'.format(
+ configurator.variables['package_folder'],
+ )
+ file_name = u'configure.zcml'
+ example_file_name = '{0}.example'.format(file_name)
+ match_xpath = "./subscriber[@handler='.{0}.handler']".format(
+ configurator.variables['subscriber_handler_file_name'],
+ )
+ match_str = '-*- extra stuff goes here -*-'
+ insert_str = """
+
+""".format(
+ configurator.variables['subscriber_handler_file_name'],
+ )
+ update_configure_zcml(
+ configurator,
+ path,
+ file_name=file_name,
+ example_file_name=example_file_name,
+ match_xpath=match_xpath,
+ match_str=match_str,
+ insert_str=insert_str,
+ )
+
+
+def _remove_unwanted_files(configurator):
+ file_paths = []
+ rel_file_paths = [
+ '/subscribers/configure.zcml.example',
+ ]
+ base_path = configurator.variables['package_folder']
+ for rel_file_path in rel_file_paths:
+ file_paths.append('{0}{1}'.format(base_path, rel_file_path))
+ remove_unwanted_files(file_paths)
+
+
+def pre_renderer(configurator):
+ """Pre rendering."""
+ configurator = base_prepare_renderer(configurator)
+ configurator.variables['template_id'] = 'subscriber'
+ name = configurator.variables['subscriber_handler_name'].strip('_')
+ configurator.variables['subscriber_handler_file_name'] = cc.snakecase(name)
+ configurator.target_directory = configurator.variables['package_folder']
+
+
+def post_renderer(configurator):
+ """Post rendering."""
+ _update_package_configure_zcml(configurator)
+ _update_subscribers_configure_zcml(configurator)
+ _remove_unwanted_files(configurator)
+ git_commit(
+ configurator,
+ 'Add subscriber: {0}'.format(
+ configurator.variables['subscriber_handler_name'],
+ ),
+ )
diff --git a/bobtemplates/plone/subscriber/.mrbob.ini b/bobtemplates/plone/subscriber/.mrbob.ini
new file mode 100644
index 00000000..a19b3a8a
--- /dev/null
+++ b/bobtemplates/plone/subscriber/.mrbob.ini
@@ -0,0 +1,20 @@
+[questions]
+subtemplate_warning.question = Please commit your changes, before using a sub-template! Continue anyway? [n/y]
+subtemplate_warning.required = True
+subtemplate_warning.default = n
+subtemplate_warning.pre_ask_question = bobtemplates.plone.base:git_clean_state_check
+subtemplate_warning.post_ask_question = mrbob.hooks:validate_choices bobtemplates.plone.base:subtemplate_warning_post_question
+subtemplate_warning.choices = y|n
+subtemplate_warning.choices_delimiter = |
+
+subscriber_handler_name.question = Subscriber handler file name (without extension)
+subscriber_handler_name.help = The name of the handler called when the event happend.
+subscriber_handler_name.default = obj_modified_do_something
+subscriber_handler_name.required = True
+subscriber_handler_name.pre_ask_question = bobtemplates.plone.base:check_root_folder
+subscriber_handler_name.post_ask_question = bobtemplates.plone.base:check_klass_name
+
+[template]
+post_ask = bobtemplates.plone.base:set_global_vars
+pre_render = bobtemplates.plone.subscriber:pre_renderer
+post_render = bobtemplates.plone.subscriber:post_renderer
diff --git a/bobtemplates/plone/subscriber/subscribers/+subscriber_handler_file_name+.py.bob b/bobtemplates/plone/subscriber/subscribers/+subscriber_handler_file_name+.py.bob
new file mode 100644
index 00000000..09c83ac4
--- /dev/null
+++ b/bobtemplates/plone/subscriber/subscribers/+subscriber_handler_file_name+.py.bob
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+
+def handler(obj, event):
+ """ Event handler
+ """
+ print(u"{0} on object {1}".format(event.__class__, obj.absolute_url()))
diff --git a/bobtemplates/plone/subscriber/subscribers/__init__.py b/bobtemplates/plone/subscriber/subscribers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bobtemplates/plone/subscriber/subscribers/configure.zcml.example.bob b/bobtemplates/plone/subscriber/subscribers/configure.zcml.example.bob
new file mode 100644
index 00000000..87466dd3
--- /dev/null
+++ b/bobtemplates/plone/subscriber/subscribers/configure.zcml.example.bob
@@ -0,0 +1,8 @@
+
+
+ -*- extra stuff goes here -*-
+
+
+
diff --git a/bobtemplates/plone/subscriber/tests/test_subscriber_+subscriber_handler_file_name+.py.bob b/bobtemplates/plone/subscriber/tests/test_subscriber_+subscriber_handler_file_name+.py.bob
new file mode 100644
index 00000000..5632c9ba
--- /dev/null
+++ b/bobtemplates/plone/subscriber/tests/test_subscriber_+subscriber_handler_file_name+.py.bob
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from {{{package.dottedname}}}.testing import {{{package.uppercasename}}}_FUNCTIONAL_TESTING
+from {{{package.dottedname}}}.testing import {{{package.uppercasename}}}_INTEGRATION_TESTING
+from plone.app.testing import setRoles
+from plone.app.testing import TEST_USER_ID
+
+import unittest
+
+
+class SubscriberIntegrationTest(unittest.TestCase):
+
+ layer = {{{package.uppercasename}}}_INTEGRATION_TESTING
+
+ def setUp(self):
+ self.portal = self.layer['portal']
+ setRoles(self.portal, TEST_USER_ID, ['Manager'])
+
+
+class SubscriberFunctionalTest(unittest.TestCase):
+
+ layer = {{{package.uppercasename}}}_FUNCTIONAL_TESTING
+
+ def setUp(self):
+ self.portal = self.layer['portal']
+ setRoles(self.portal, TEST_USER_ID, ['Manager'])
diff --git a/package_tests/test_subscriber.py b/package_tests/test_subscriber.py
new file mode 100644
index 00000000..059fa19a
--- /dev/null
+++ b/package_tests/test_subscriber.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+
+"""Test view generation."""
+
+from bobtemplates.plone import base
+from bobtemplates.plone import subscriber
+from mrbob.bobexceptions import ValidationError
+from mrbob.configurator import Configurator
+
+import os
+import pytest
+
+
+def test_update_subscribers_configure_zcml(tmpdir):
+ """Test configure changes when changes are already in place."""
+ target_path = tmpdir.strpath + '/collective.sample'
+ package_path = target_path + '/src/collective/sample'
+ subscribers_path = package_path + '/subscribers/'
+ os.makedirs(target_path)
+ os.makedirs(package_path)
+ os.makedirs(subscribers_path)
+ template = """
+
+ -*- extra stuff goes here -*-
+
+
+
+
+"""
+ with open(os.path.join(subscribers_path + 'configure.zcml'), 'w') as f:
+ f.write(template)
+ configurator = Configurator(
+ template='bobtemplates.plone:subscriber',
+ target_directory='collective.sample',
+ bobconfig={
+ 'non_interactive': True,
+ },
+ variables={
+ 'subscriber_handler_name': 'obj_mod_do_something',
+ 'subscriber_handler_file_name': 'obj_mod_do_something',
+ 'package_folder': package_path,
+ },
+ )
+ subscriber._update_subscribers_configure_zcml(configurator)
+
+ with open(os.path.join(subscribers_path + 'configure.zcml'), 'r') as f:
+ content = f.read()
+ if content != template:
+ pytest.raises(ValidationError)
+