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 =