diff --git a/CHANGES.rst b/CHANGES.rst
index 2905b9b0..db5fde77 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,7 +1,7 @@
Changelog
=========
-3.5.3 (unreleased)
+3.6.0b1 (unreleased)
------------------
- Add support for Python 3.
@@ -10,6 +10,15 @@ Changelog
- Replace portal_quickinstaller in tests for Plone 5.1+.
[pbauer]
+- Avoid linty issues in zcml files in updateing method for zcml files
+ [MrTango]
+
+- provide generic methods remove_unwanted_files/update_configure_zcml
+ [MrTango]
+
+- Add restapi_service sub-template
+ [MrTango]
+
3.5.2 (2018-10-30)
------------------
diff --git a/bobtemplates/plone/base.py b/bobtemplates/plone/base.py
index 763a0b79..ab85320f 100644
--- a/bobtemplates/plone/base.py
+++ b/bobtemplates/plone/base.py
@@ -2,6 +2,7 @@
from colorama import Fore
from colorama import Style
from datetime import date
+from lxml import etree
from mrbob import hooks
from mrbob.bobexceptions import MrBobError
from mrbob.bobexceptions import SkipQuestion
@@ -337,6 +338,40 @@ def make_path(*args):
return os.sep.join(args)
+def update_configure_zcml(
+ configurator,
+ path,
+ file_name=None,
+ example_file_name=None,
+ match_xpath=None,
+ match_str=None,
+ insert_str=None,
+):
+ if path[-1] != '/':
+ path += '/'
+ file_path = os.path.join(path, file_name)
+ if example_file_name:
+ example_file_path = os.path.join(path, example_file_name)
+ file_list = os.listdir(os.path.dirname(path))
+ if file_name not in file_list:
+ print('rename example zcml file')
+ os.rename(example_file_path, file_path)
+ namespaces = '{http://namespaces.zope.org/zope}'
+ with open(file_path, 'r') as xml_file:
+ parser = etree.XMLParser(remove_blank_text=True)
+ tree = etree.parse(xml_file, parser)
+ tree_root = tree.getroot()
+ match_xpath_ns = '{0}{1}'.format(namespaces, match_xpath)
+ if len(tree_root.findall(match_xpath_ns)):
+ print(
+ '{0} already in configure.zcml, skip adding!'.format(
+ insert_str,
+ ),
+ )
+ return
+ update_file(configurator, file_path, match_str, insert_str)
+
+
def update_file(configurator, file_path, match_str, insert_str):
"""Insert insert_str into given file, by match_str."""
changed = False
@@ -432,6 +467,13 @@ def base_prepare_renderer(configurator):
return configurator
+def remove_unwanted_files(file_paths):
+ for file_path in file_paths:
+ if not os.path.isfile(file_path):
+ continue
+ os.remove(file_path)
+
+
def subtemplate_warning(configurator, question):
"""Show a warning to the user before using subtemplates!"""
print("""
diff --git a/bobtemplates/plone/bobregistry.py b/bobtemplates/plone/bobregistry.py
index 1cd76c3d..467b91d2 100644
--- a/bobtemplates/plone/bobregistry.py
+++ b/bobtemplates/plone/bobregistry.py
@@ -96,3 +96,11 @@ def plone_behavior():
reg.plonecli_alias = 'behavior'
reg.depend_on = 'plone_addon'
return reg
+
+
+def plone_restapi_service():
+ reg = RegEntry()
+ reg.template = 'bobtemplates.plone:restapi_service'
+ reg.plonecli_alias = 'restapi_service'
+ reg.depend_on = 'plone_addon'
+ return reg
diff --git a/bobtemplates/plone/restapi_service.py b/bobtemplates/plone/restapi_service.py
new file mode 100644
index 00000000..89ed482a
--- /dev/null
+++ b/bobtemplates/plone/restapi_service.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+
+from bobtemplates.plone.base import base_prepare_renderer
+from bobtemplates.plone.base import git_commit
+from bobtemplates.plone.base import is_string_in_file
+from bobtemplates.plone.base import remove_unwanted_files
+from bobtemplates.plone.base import update_configure_zcml
+from bobtemplates.plone.base import update_file
+
+import case_conversion as cc
+
+
+# from mrbob.bobexceptions import SkipQuestion
+# from mrbob.bobexceptions import ValidationError
+
+
+def get_service_name_from_python_class(configurator, question):
+ """Get default service_name from python class"""
+ class_name = configurator.variables['service_class_name']
+ if class_name:
+ generated_name = cc.snakecase(class_name).replace('_', '-')
+ question.default = generated_name
+ else:
+ question.default = 'my-service'
+
+
+def _update_package_configure_zcml(configurator):
+ path = '{0}'.format(
+ configurator.variables['package_folder'],
+ )
+ file_name = u'configure.zcml'
+ match_xpath = "include[@package='.api']"
+ 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_api_configure_zcml(configurator):
+ path = '{0}/api'.format(
+ configurator.variables['package_folder'],
+ )
+ file_name = u'configure.zcml'
+ example_file_name = '{0}.example'.format(file_name)
+ match_xpath = "include[@package='.services']"
+ match_str = '-*- extra stuff goes here -*-'
+ insert_str = """
+
+"""
+ 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 _update_services_configure_zcml(configurator):
+ path = '{0}/api/services'.format(
+ configurator.variables['package_folder'],
+ )
+ file_name = u'configure.zcml'
+ example_file_name = '{0}.example'.format(file_name)
+ match_xpath = "include[@package='.{0}']".format(
+ configurator.variables['service_class_name_normalized'],
+ )
+ match_str = '-*- extra stuff goes here -*-'
+ insert_str = '\n'.format(
+ configurator.variables['service_class_name_normalized'],
+ )
+ 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 _update_setup_py(configurator):
+ file_name = u'setup.py'
+ file_path = configurator.variables['package.root_folder'] + '/' + file_name
+ match_str = '-*- Extra requirements: -*-'
+ insert_strings = [
+ 'plone.restapi',
+ ]
+ for insert_str in insert_strings:
+ insert_str = " '{0}',\n".format(insert_str)
+ if is_string_in_file(configurator, file_path, insert_str):
+ continue
+ update_file(configurator, file_path, match_str, insert_str)
+
+
+def _remove_unwanted_files(configurator):
+ file_paths = []
+ rel_file_paths = [
+ '/api/configure.zcml.example',
+ '/api/services/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'] = 'restapi_service'
+ name = configurator.variables['service_name'].strip('_')
+ name_normalized = cc.snakecase(name)
+ configurator.variables['service_name_normalized'] = name_normalized
+ class_name = configurator.variables['service_class_name'].strip('_') # NOQA: E501
+ configurator.variables['service_class_name'] = cc.pascalcase( # NOQA: E501
+ class_name,
+ )
+ configurator.variables['service_class_name_normalized'] = cc.snakecase(
+ class_name,
+ )
+ configurator.target_directory = configurator.variables['package_folder']
+
+
+def post_renderer(configurator):
+ """Post rendering."""
+ _update_package_configure_zcml(configurator)
+ _update_api_configure_zcml(configurator)
+ _update_services_configure_zcml(configurator)
+ # _remove_unwanted_files(configurator)
+ git_commit(
+ configurator,
+ 'Add restapi_service: {0}'.format(
+ configurator.variables['service_name'],
+ ),
+ )
diff --git a/bobtemplates/plone/restapi_service/.mrbob.ini b/bobtemplates/plone/restapi_service/.mrbob.ini
new file mode 100644
index 00000000..f931c725
--- /dev/null
+++ b/bobtemplates/plone/restapi_service/.mrbob.ini
@@ -0,0 +1,25 @@
+[questions]
+subtemplate_warning.question = Please commit your changes, before using a sub-template! Continue anyway? (y/n)
+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 = |
+
+service_class_name.question = Service class name
+service_class_name.help = Should be something like 'RelatedThings' (PascalCase)
+service_class_name.required = True
+service_class_name.default = RelatedThings
+service_class_name.post_ask_question = bobtemplates.plone.base:check_klass_name
+
+service_name.question = Service name
+service_name.help = Should be something like 'related-things' (URL slug)
+service_name.required = True
+# service_name.default = related-things
+service_name.pre_ask_question = bobtemplates.plone.restapi_service:get_service_name_from_python_class
+
+[template]
+pre_render = bobtemplates.plone.restapi_service:pre_renderer
+post_render = bobtemplates.plone.restapi_service:post_renderer
+post_ask = bobtemplates.plone.base:set_global_vars
diff --git a/bobtemplates/plone/restapi_service/api/__init__.py b/bobtemplates/plone/restapi_service/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bobtemplates/plone/restapi_service/api/configure.zcml.example.bob b/bobtemplates/plone/restapi_service/api/configure.zcml.example.bob
new file mode 100644
index 00000000..5841c5f6
--- /dev/null
+++ b/bobtemplates/plone/restapi_service/api/configure.zcml.example.bob
@@ -0,0 +1,10 @@
+
+
+ -*- extra stuff goes here -*-
+
+
+
diff --git a/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/__init__.py b/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/configure.zcml.bob b/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/configure.zcml.bob
new file mode 100644
index 00000000..6485c3d3
--- /dev/null
+++ b/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/configure.zcml.bob
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/get.py.bob b/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/get.py.bob
new file mode 100644
index 00000000..760baa41
--- /dev/null
+++ b/bobtemplates/plone/restapi_service/api/services/+service_class_name_normalized+/get.py.bob
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+from plone import api
+from plone.restapi.interfaces import IExpandableElement
+from plone.restapi.services import Service
+from zope.component import adapter
+from zope.interface import Interface
+from zope.interface import implementer
+
+
+@implementer(IExpandableElement)
+@adapter(Interface, Interface)
+class {{{ service_class_name }}}(object):
+
+ def __init__(self, context, request):
+ self.context = context.aq_explicit
+ self.request = request
+
+ def __call__(self, expand=False):
+ result = {
+ '{{{ service_name_normalized }}}': {
+ '@id': '{}/@{{{ service_name_normalized }}}'.format(
+ self.context.absolute_url(),
+ ),
+ },
+ }
+ if not expand:
+ return result
+
+ # === Your custom code comes here ===
+
+ # Example:
+ query = {}
+ query['portal_type'] = "Document"
+ query['Subject'] = {
+ 'query': ['Cats', 'Dogs'],
+ 'operator': 'or',
+ }
+ brains = api.content.find(**query)
+ items = []
+ for brain in brains:
+ obj = brain.getObject()
+ parent = obj.aq_inner.aq_parent
+ items.append({
+ 'title': brain.Title,
+ 'description': brain.Description,
+ '@id': brain.getURL(),
+ })
+ result['{{{ service_name_normalized }}}']['items'] = items
+ return result
+
+
+class {{{ service_class_name }}}Get(Service):
+
+ def reply(self):
+ service_factory = {{{ service_class_name }}}(self.context, self.request)
+ return service_factory(expand=True)['{{{ service_name_normalized }}}']
diff --git a/bobtemplates/plone/restapi_service/api/services/__init__.py b/bobtemplates/plone/restapi_service/api/services/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bobtemplates/plone/restapi_service/api/services/configure.zcml.example.bob b/bobtemplates/plone/restapi_service/api/services/configure.zcml.example.bob
new file mode 100644
index 00000000..5841c5f6
--- /dev/null
+++ b/bobtemplates/plone/restapi_service/api/services/configure.zcml.example.bob
@@ -0,0 +1,10 @@
+
+
+ -*- extra stuff goes here -*-
+
+
+
diff --git a/package-tests/test_restapi_service.py b/package-tests/test_restapi_service.py
new file mode 100644
index 00000000..4212fbbc
--- /dev/null
+++ b/package-tests/test_restapi_service.py
@@ -0,0 +1,264 @@
+# -*- coding: utf-8 -*-
+
+from bobtemplates.plone import base
+from bobtemplates.plone import restapi_service
+from mrbob.configurator import Configurator
+
+import os
+
+
+def test_pre_renderer(tmpdir):
+ target_path = tmpdir.strpath + '/collective.todo'
+ package_path = target_path + '/src/collective/todo'
+ os.makedirs(target_path)
+ os.makedirs(package_path)
+ template = """
+[main]
+version=5.1
+"""
+ with open(os.path.join(target_path + '/bobtemplate.cfg'), 'w') as f:
+ f.write(template)
+
+ template = """
+ dummy
+ '-*- Extra requirements: -*-'
+"""
+ with open(os.path.join(target_path + '/setup.py'), 'w') as f:
+ f.write(template)
+
+ configurator = Configurator(
+ template='bobtemplates.plone:restapi_service',
+ target_directory=target_path,
+ variables={
+ 'service_class_name': 'SomeRelatedThings',
+ 'service_name': 'some-related-things',
+ 'package_folder': package_path,
+ },
+ )
+ restapi_service.pre_renderer(configurator)
+
+
+def test_post_renderer(tmpdir):
+ target_path = tmpdir.strpath + '/collective.todo'
+ package_path = target_path + '/src/collective/todo'
+ profiles_path = package_path + '/profiles/default'
+ os.makedirs(target_path)
+ os.makedirs(package_path)
+ os.makedirs(profiles_path)
+
+ template = """
+
+ 1000
+
+
+
+
+"""
+ with open(os.path.join(profiles_path + '/metadata.xml'), 'w') as f:
+ f.write(template)
+
+ template = """
+[main]
+version=5.1
+"""
+ with open(os.path.join(target_path + '/bobtemplate.cfg'), 'w') as f:
+ f.write(template)
+
+ template = """
+ dummy
+ '-*- Extra requirements: -*-'
+"""
+ with open(os.path.join(target_path + '/setup.py'), 'w') as f:
+ f.write(template)
+
+ template = """
+
+
+
+
+
+"""
+ with open(os.path.join(package_path + '/configure.zcml'), 'w') as f:
+ f.write(template)
+
+ configurator = Configurator(
+ template='bobtemplates.plone:restapi_service',
+ target_directory=package_path,
+ bobconfig={
+ 'non_interactive': True,
+ },
+ variables={
+ 'package_folder': package_path,
+ 'plone.version': '5.1',
+ 'service_class_name': 'SomeRelatedThings',
+ 'service_name': 'some-related-things',
+ },
+ )
+ assert configurator
+ os.chdir(package_path)
+ base.set_global_vars(configurator)
+ restapi_service.pre_renderer(configurator)
+ configurator.render()
+ restapi_service.post_renderer(configurator)
+
+
+def test_remove_unwanted_files(tmpdir):
+ files_to_remove = [
+ '/api/configure.zcml.example',
+ '/api/services/configure.zcml.example',
+ ]
+ target_path = tmpdir.strpath + '/collective.todo'
+ package_path = target_path + '/src/collective/todo'
+ os.makedirs(package_path + '/api/services/')
+ configurator = Configurator(
+ template='bobtemplates.plone:restapi_service',
+ target_directory=tmpdir.strpath,
+ variables={
+ 'package_folder': package_path,
+ },
+ )
+ for file_to_remove in files_to_remove:
+ with open(
+ os.path.join(
+ package_path + file_to_remove,
+ ),
+ 'w',
+ ) as f:
+ f.write(u'dummy')
+ restapi_service._remove_unwanted_files(configurator)
+
+ for file_to_remove in files_to_remove:
+ assert not os.path.isfile(
+ os.path.join(package_path + file_to_remove),
+ )
+
+
+def test_update_api_configure_zcml(tmpdir):
+ """
+ """
+ target_path = tmpdir.strpath + '/collective.todo'
+ package_path = target_path + '/src/collective/todo'
+ os.makedirs(package_path + '/api/')
+
+ template = """
+[main]
+version=5.1
+"""
+ with open(os.path.join(target_path + '/bobtemplate.cfg'), 'w') as f:
+ f.write(template)
+
+ template = """
+ dummy
+ '-*- Extra requirements: -*-'
+"""
+ with open(os.path.join(target_path + '/setup.py'), 'w') as f:
+ f.write(template)
+
+ template = """
+
+
+
+
+
+"""
+ with open(os.path.join(package_path + '/configure.zcml'), 'w') as f:
+ f.write(template)
+ with open(os.path.join(package_path + '/api/configure.zcml'), 'w') as f:
+ f.write(template)
+ configurator = Configurator(
+ template='bobtemplates.plone:restapi_service',
+ target_directory=package_path,
+ bobconfig={
+ 'non_interactive': True,
+ },
+ variables={
+ 'package_folder': package_path,
+ 'plone.version': '5.1',
+ 'service_class_name': 'SomeRelatedThings',
+ 'service_name': 'some-related-things',
+ },
+ )
+ restapi_service._update_api_configure_zcml(configurator)
+
+ with open(
+ os.path.join(
+ package_path + '/api/configure.zcml',
+ ),
+ 'r',
+ ) as f:
+ content = f.read()
+ assert content != template, u'configure.zcml was not updated!'
+
+
+def test_update_services_configure_zcml(tmpdir):
+ """
+ """
+ target_path = tmpdir.strpath + '/collective.todo'
+ package_path = target_path + '/src/collective/todo'
+ os.makedirs(package_path + '/api/services/')
+
+ template = """
+[main]
+version=5.1
+"""
+ with open(os.path.join(target_path + '/bobtemplate.cfg'), 'w') as f:
+ f.write(template)
+
+ template = """
+ dummy
+ '-*- Extra requirements: -*-'
+"""
+ with open(os.path.join(target_path + '/setup.py'), 'w') as f:
+ f.write(template)
+
+ template = """
+
+
+
+
+
+"""
+ with open(os.path.join(package_path + '/configure.zcml'), 'w') as f:
+ f.write(template)
+ with open(os.path.join(package_path + '/api/configure.zcml'), 'w') as f:
+ f.write(template)
+ with open(
+ os.path.join(package_path + '/api/services/configure.zcml'), 'w',
+ ) as f:
+ f.write(template)
+ configurator = Configurator(
+ template='bobtemplates.plone:restapi_service',
+ target_directory=package_path,
+ bobconfig={
+ 'non_interactive': True,
+ },
+ variables={
+ 'package_folder': package_path,
+ 'plone.version': '5.1',
+ 'service_class_name': 'SomeRelatedThings',
+ 'service_class_name_normalized': 'some_related_things',
+ 'service_name': 'some-related-things',
+ },
+ )
+ restapi_service._update_services_configure_zcml(configurator)
+
+ with open(
+ os.path.join(
+ package_path + '/api/services/configure.zcml',
+ ),
+ 'r',
+ ) as f:
+ content = f.read()
+ assert content != template, u'configure.zcml was not updated!'
diff --git a/setup.py b/setup.py
index 0b4b2056..e7272ad2 100644
--- a/setup.py
+++ b/setup.py
@@ -67,6 +67,7 @@
'plone_theme_barceloneta = bobtemplates.plone.bobregistry:plone_theme_barceloneta', # NOQA E501
'plone_vocabulary = bobtemplates.plone.bobregistry:plone_vocabulary', # NOQA E501
'plone_behavior = bobtemplates.plone.bobregistry:plone_behavior', # NOQA E501
+ 'plone_restapi_service = bobtemplates.plone.bobregistry:plone_restapi_service', # NOQA E501
],
},
)
diff --git a/tox.ini b/tox.ini
index a3465075..5dc0cb63 100644
--- a/tox.ini
+++ b/tox.ini
@@ -14,6 +14,7 @@ envlist =
py{27}-skeletontests-Plone-{4.3,5.0,5.1}-template-addon_vocabulary
py{27}-skeletontests-Plone-{4.3,5.0,5.1}-template-addon_behavior
py{27}-skeletontests-Plone-{4.3,5.0,5.1}-template-theme_package
+ # py{27}-skeletontests-Plone-{4.3,5.0,5.1}-template-addon_restapi_service
docs,
coverage-report,
@@ -40,6 +41,7 @@ commands =
template-addon_theme: pytest skeleton-tests/test_addon_theme.py {posargs}
template-addon_vocabulary: pytest skeleton-tests/test_addon_vocabulary.py {posargs}
template-addon_behavior: pytest skeleton-tests/test_addon_behavior.py {posargs}
+ template-addon_restapi_service: pytest skeleton-tests/test_addon_restapi_service.py {posargs}
template-theme_package: pytest skeleton-tests/test_theme_package.py {posargs}
setenv =