Skip to content

Commit

Permalink
Merge pull request #347 from plone/mrtango-restapi-service
Browse files Browse the repository at this point in the history
Add restapi service sub-template
  • Loading branch information
MrTango committed Dec 28, 2018
2 parents 0bd258c + c4511c3 commit 07ac965
Show file tree
Hide file tree
Showing 15 changed files with 590 additions and 1 deletion.
11 changes: 10 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Changelog
=========

3.5.3 (unreleased)
3.6.0b1 (unreleased)
------------------

- Add support for Python 3.
Expand All @@ -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)
------------------
Expand Down
42 changes: 42 additions & 0 deletions bobtemplates/plone/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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("""
Expand Down
8 changes: 8 additions & 0 deletions bobtemplates/plone/bobregistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
147 changes: 147 additions & 0 deletions bobtemplates/plone/restapi_service.py
Original file line number Diff line number Diff line change
@@ -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 = """
<include package=".api" />
"""
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 = """
<include package=".services" />
"""
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 = '<include package=".{0}" />\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'],
),
)
25 changes: 25 additions & 0 deletions bobtemplates/plone/restapi_service/.mrbob.ini
Original file line number Diff line number Diff line change
@@ -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
Empty file.
10 changes: 10 additions & 0 deletions bobtemplates/plone/restapi_service/api/configure.zcml.example.bob
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="{{{ package.dottedname }}}">

-*- extra stuff goes here -*-


</configure>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:plone="http://namespaces.plone.org/plone">

<adapter factory=".get.{{{ service_class_name }}}" name="{{{ service_name_normalized }}}"/>

<plone:service
method="GET"
for="zope.interface.Interface"
factory=".get.{{{ service_class_name }}}"
name="@{{{ service_name_normalized }}}"
permission="zope2.View"
/>

</configure>
Original file line number Diff line number Diff line change
@@ -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 }}}']
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
xmlns:plone="http://namespaces.plone.org/plone"
i18n_domain="{{{ package.dottedname }}}">

-*- extra stuff goes here -*-


</configure>
Loading

0 comments on commit 07ac965

Please sign in to comment.